diff --git a/lib/src/generator/templates.runtime_renderers.dart b/lib/src/generator/templates.runtime_renderers.dart index f7a8c98686..00e51acf4f 100644 --- a/lib/src/generator/templates.runtime_renderers.dart +++ b/lib/src/generator/templates.runtime_renderers.dart @@ -7746,6 +7746,19 @@ class _Renderer_InheritingContainer extends RendererBase { self.renderSimpleVariable(c, remainingNames, 'bool'), getBool: (CT_ c) => c.publicInheritedInstanceOperators, ), + 'publicInterfaceElements': Property( + getValue: (CT_ c) => c.publicInterfaceElements, + renderVariable: (CT_ c, Property self, + List remainingNames) => + self.renderSimpleVariable( + c, remainingNames, 'Iterable'), + renderIterable: (CT_ c, RendererBase r, + List ast, StringSink sink) { + return c.publicInterfaceElements.map((e) => + _render_InheritingContainer(e, ast, r.template, sink, + parent: r)); + }, + ), 'publicInterfaces': Property( getValue: (CT_ c) => c.publicInterfaces, renderVariable: (CT_ c, Property self, @@ -9934,17 +9947,17 @@ class _Renderer_MixedInTypes extends RendererBase { self.renderSimpleVariable(c, remainingNames, 'bool'), getBool: (CT_ c) => c.hasPublicMixedInTypes, ), - 'mixedInTypes': Property( - getValue: (CT_ c) => c.mixedInTypes, + 'mixedInElements': Property( + getValue: (CT_ c) => c.mixedInElements, renderVariable: (CT_ c, Property self, List remainingNames) => self.renderSimpleVariable( - c, remainingNames, 'List'), + c, remainingNames, 'List'), renderIterable: (CT_ c, RendererBase r, List ast, StringSink sink) { - return c.mixedInTypes.map((e) => _render_DefinedElementType( - e, ast, r.template, sink, - parent: r)); + return c.mixedInElements.map((e) => + _render_InheritingContainer(e, ast, r.template, sink, + parent: r)); }, ), 'publicMixedInTypes': Property( @@ -15267,6 +15280,19 @@ class _Renderer_TypeImplementing extends RendererBase { self.renderSimpleVariable(c, remainingNames, 'bool'), getBool: (CT_ c) => c.hasPublicInterfaces, ), + 'interfaceElements': Property( + getValue: (CT_ c) => c.interfaceElements, + renderVariable: (CT_ c, Property self, + List remainingNames) => + self.renderSimpleVariable( + c, remainingNames, 'List'), + renderIterable: (CT_ c, RendererBase r, + List ast, StringSink sink) { + return c.interfaceElements.map((e) => + _render_InheritingContainer(e, ast, r.template, sink, + parent: r)); + }, + ), 'interfaces': Property( getValue: (CT_ c) => c.interfaces, renderVariable: (CT_ c, Property self, diff --git a/lib/src/model/class.dart b/lib/src/model/class.dart index 5e039bdfd7..f61af52c4d 100644 --- a/lib/src/model/class.dart +++ b/lib/src/model/class.dart @@ -31,7 +31,7 @@ class Class extends InheritingContainer this, // Caching should make this recursion a little less painful. - for (var container in mixedInTypes.reversed.modelElements) + for (var container in mixedInElements.reversed) ...container.inheritanceChain, for (var container in superChain.modelElements) @@ -39,7 +39,7 @@ class Class extends InheritingContainer // Interfaces need to come last, because classes in the superChain might // implement them even when they aren't mentioned. - ...interfaces.expandInheritanceChain, + ...interfaceElements.expandInheritanceChain, ]; Class(this.element, Library library, PackageGraph packageGraph) diff --git a/lib/src/model/enum.dart b/lib/src/model/enum.dart index a43b6987a8..ffd8303e52 100644 --- a/lib/src/model/enum.dart +++ b/lib/src/model/enum.dart @@ -27,11 +27,11 @@ class Enum extends InheritingContainer @override late final List inheritanceChain = [ this, - for (var container in mixedInTypes.reversed.modelElements) + for (var container in mixedInElements.reversed) ...container.inheritanceChain, for (var container in superChain.modelElements) ...container.inheritanceChain, - ...interfaces.expandInheritanceChain, + ...interfaceElements.expandInheritanceChain, ]; @override diff --git a/lib/src/model/extension_type.dart b/lib/src/model/extension_type.dart index 0d1e6f0666..e4a4f76b11 100644 --- a/lib/src/model/extension_type.dart +++ b/lib/src/model/extension_type.dart @@ -63,7 +63,7 @@ class ExtensionType extends InheritingContainer @override late final List inheritanceChain = [ this, - ...interfaces.expandInheritanceChain, + ...interfaceElements.expandInheritanceChain, ]; @override diff --git a/lib/src/model/inheriting_container.dart b/lib/src/model/inheriting_container.dart index 39f97e9a20..0f73a596ec 100644 --- a/lib/src/model/inheriting_container.dart +++ b/lib/src/model/inheriting_container.dart @@ -370,7 +370,12 @@ abstract class InheritingContainer extends Container Iterable get publicInheritedMethods => model_utils.filterNonPublic(inheritedMethods); - Iterable get publicInterfaces => const []; + Iterable get publicInterfaces; + + Iterable get publicInterfaceElements => [ + for (var interface in publicInterfaces) + interface.modelElement as InheritingContainer, + ]; Iterable get publicSuperChainReversed => publicSuperChain.reversed; @@ -474,10 +479,15 @@ abstract class InheritingContainer extends Container /// Add the ability to support mixed-in types to an [InheritingContainer]. mixin MixedInTypes on InheritingContainer { + @visibleForTesting late final List mixedInTypes = element.mixins .map((f) => modelBuilder.typeFrom(f, library) as DefinedElementType) .toList(growable: false); + List get mixedInElements => [ + for (var t in mixedInTypes) t.modelElement as InheritingContainer, + ]; + @override bool get hasModifiers => super.hasModifiers || hasPublicMixedInTypes; @@ -509,9 +519,14 @@ mixin TypeImplementing on InheritingContainer { /// Interfaces directly implemented by this container. List get interfaces => _directInterfaces; - /// Returns all the "immediate" public implementors of this - /// [TypeImplementing]. For a [Mixin], this is actually the mixin - /// applications using the [Mixin]. + List get interfaceElements => [ + for (var interface in interfaces) + interface.modelElement as InheritingContainer, + ]; + + /// All the "immediate" public implementors of this [TypeImplementing]. + /// + /// For a [Mixin], this is actually the mixin applications using the [Mixin]. /// /// If this [InheritingContainer] has a private implementor, then that is /// counted as a proxy for any public implementors of that private container. @@ -527,16 +542,17 @@ mixin TypeImplementing on InheritingContainer { if (implementor.isPublicAndPackageDocumented) { result.add(implementor); } else { - model_utils - .findCanonicalFor( - packageGraph.implementors[implementor] ?? const []) - .forEach(addToResult); + var implementors = packageGraph.implementors[implementor]; + if (implementors != null) { + model_utils.findCanonicalFor(implementors).forEach(addToResult); + } } } - model_utils - .findCanonicalFor(packageGraph.implementors[this] ?? const []) - .forEach(addToResult); + var immediateImplementors = packageGraph.implementors[this]; + if (immediateImplementors != null) { + model_utils.findCanonicalFor(immediateImplementors).forEach(addToResult); + } return result; } @@ -587,11 +603,14 @@ extension on InterfaceElement { } extension DefinedElementTypeIterableExtension on Iterable { - /// Expands the [ModelElement] for each element to its inheritance chain. - Iterable get expandInheritanceChain => - expand((e) => (e.modelElement as InheritingContainer).inheritanceChain); - - /// Returns the [ModelElement] for each element. + /// The [ModelElement] for each element. Iterable get modelElements => map((e) => e.modelElement as InheritingContainer); } + +extension InheritingContainerIterableExtension + on Iterable { + /// Expands each element to its inheritance chain. + Iterable get expandInheritanceChain => + expand((e) => e.inheritanceChain); +} diff --git a/lib/src/model/mixin.dart b/lib/src/model/mixin.dart index e2ce4c343e..318879f31d 100644 --- a/lib/src/model/mixin.dart +++ b/lib/src/model/mixin.dart @@ -31,14 +31,14 @@ class Mixin extends InheritingContainer with TypeImplementing { this, // Mix-in interfaces come before other interfaces. - ...superclassConstraints.expandInheritanceChain, + ...superclassConstraints.modelElements.expandInheritanceChain, for (var container in superChain.modelElements) ...container.inheritanceChain, // Interfaces need to come last, because classes in the superChain might // implement them even when they aren't mentioned. - ...interfaces.expandInheritanceChain, + ...interfaceElements.expandInheritanceChain, ]; Mixin(this.element, super.library, super.packageGraph); diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart index 4dd0159116..7ff85c8f0c 100644 --- a/lib/src/model/package_graph.dart +++ b/lib/src/model/package_graph.dart @@ -139,8 +139,9 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder { // all packages are picked up. for (var package in _documentedPackages) { for (var library in package.libraries) { - _addToImplementors(library.allClasses); - _addToImplementors(library.mixins); + _addToImplementers(library.allClasses); + _addToImplementers(library.mixins); + _addToImplementers(library.extensionTypes); _extensions.addAll(library.extensions); } if (package.isLocal && !package.hasPublicLibraries) { @@ -580,7 +581,7 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder { return hrefMap; } - void _addToImplementors(Iterable containers) { + void _addToImplementers(Iterable containers) { assert(!allImplementorsAdded); // Private containers may not be included in documentation, but may still be @@ -589,7 +590,7 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder { var privates = []; void checkAndAddContainer( - InheritingContainer implemented, InheritingContainer implementor) { + InheritingContainer implemented, InheritingContainer implementer) { if (!implemented.isPublic) { privates.add(implemented); } @@ -598,36 +599,40 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder { var list = _implementors.putIfAbsent(implemented, () => []); // TODO(srawlins): This would be more efficient if we created a // SplayTreeSet keyed off of `.element`. - if (!list.any((l) => l.element == implementor.element)) { - list.add(implementor); + if (!list.any((l) => l.element == implementer.element)) { + list.add(implementer); } } - void addImplementor(InheritingContainer clazz) { - var supertype = clazz.supertype; + void addImplementer(InheritingContainer container) { + var supertype = container.supertype; if (supertype != null) { checkAndAddContainer( - supertype.modelElement as InheritingContainer, clazz); + supertype.modelElement as InheritingContainer, container); } - if (clazz is Class) { - for (var type in clazz.mixedInTypes) { - checkAndAddContainer(type.modelElement as InheritingContainer, clazz); + if (container is Class) { + for (var element in container.mixedInElements) { + checkAndAddContainer(element, container); } - for (var type in clazz.interfaces) { - checkAndAddContainer(type.modelElement as InheritingContainer, clazz); + for (var element in container.interfaceElements) { + checkAndAddContainer(element, container); + } + } else if (container is ExtensionType) { + for (var element in container.interfaceElements) { + checkAndAddContainer(element, container); } } - for (var type in clazz.publicInterfaces) { - checkAndAddContainer(type.modelElement as InheritingContainer, clazz); + for (var element in container.publicInterfaceElements) { + checkAndAddContainer(element, container); } } - containers.forEach(addImplementor); + containers.forEach(addImplementer); // [privates] may grow while processing; use a for loop, rather than a // for-each loop, to avoid concurrent modification errors. for (var i = 0; i < privates.length; i++) { - addImplementor(privates[i]); + addImplementer(privates[i]); } } @@ -661,10 +666,10 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder { ?.linkedName ?? 'Object'; - /// The set of [Class]es which should _not_ be presented as implementors. + /// The set of [Class]es which should _not_ be presented as implementers. /// /// Add classes here if they are similar to Interceptor in that they are to be - /// ignored even when they are the implementors of [Inheritable]s, and the + /// ignored even when they are the implementers of [Inheritable]s, and the /// class these inherit from should instead claim implementation. late final Set inheritThrough = () { var interceptorSpecialClass = specialClasses[SpecialClass.interceptor]; diff --git a/test/end2end/model_test.dart b/test/end2end/model_test.dart index d448c0ded0..db8a8488aa 100644 --- a/test/end2end/model_test.dart +++ b/test/end2end/model_test.dart @@ -1820,8 +1820,8 @@ void main() { test('Verify inheritance/mixin structure and type inference', () { expect( - TypeInferenceMixedIn.mixedInTypes - .map((DefinedElementType t) => t.modelElement.name), + TypeInferenceMixedIn.mixedInElements + .map((element) => element.name), orderedEquals(['GenericMixin'])); expect( TypeInferenceMixedIn.mixedInTypes.first.typeArguments @@ -1983,15 +1983,15 @@ void main() { }); test('mixins', () { - expect(Apple.mixedInTypes, hasLength(0)); + expect(Apple.mixedInElements, hasLength(0)); }); test('mixins private', () { - expect(F.mixedInTypes, hasLength(1)); + expect(F.mixedInElements, hasLength(1)); }); test('interfaces', () { - var interfaces = Dog.interfaces; + var interfaces = Dog.interfaceElements; expect(interfaces, hasLength(2)); expect(interfaces[0].name, 'Cat'); expect(interfaces[1].name, 'E'); @@ -4399,28 +4399,37 @@ String? topLevelFunction(int param1, bool param2, Cool coolBeans, test('a class that implements Future', () { expect( - ImplementsFutureVoid.linkedName, - equals( - 'ImplementsFutureVoid')); + ImplementsFutureVoid.linkedName, + equals( + 'ImplementsFutureVoid', + ), + ); var FutureVoid = - ImplementsFutureVoid.interfaces.firstWhere((c) => c.name == 'Future'); + ImplementsFutureVoid.interfaces.firstWhere((e) => e.name == 'Future'); expect( - FutureVoid.linkedName, - equals( - 'Future<void>')); + FutureVoid.linkedName, + equals( + 'Future<void>', + ), + ); }); test('Verify that a mixin with a void type parameter works', () { expect( - ATypeTakingClassMixedIn.linkedName, - equals( - 'ATypeTakingClassMixedIn')); + ATypeTakingClassMixedIn.linkedName, + equals( + 'ATypeTakingClassMixedIn', + ), + ); var ATypeTakingClassVoid = ATypeTakingClassMixedIn.mixedInTypes - .firstWhere((c) => c.name == 'ATypeTakingClass'); + .firstWhere((e) => e.name == 'ATypeTakingClass'); expect( - ATypeTakingClassVoid.linkedName, - equals( - 'ATypeTakingClass<void>')); + ATypeTakingClassVoid.linkedName, + equals( + 'ATypeTakingClass' + '<void>', + ), + ); }); }); diff --git a/test/enum_test.dart b/test/enum_test.dart index f3621bfbfa..7ddb97e5b9 100644 --- a/test/enum_test.dart +++ b/test/enum_test.dart @@ -162,8 +162,8 @@ enum E implements C, D { one, two, three; } '''); var eEnum = library.enums.named('E'); - expect(eEnum.interfaces, hasLength(2)); - expect(eEnum.interfaces.map((i) => i.name), equals(['C', 'D'])); + expect(eEnum.interfaceElements, hasLength(2)); + expect(eEnum.interfaceElements.map((e) => e.name), equals(['C', 'D'])); } void test_linkedGenericParameters() async { @@ -214,8 +214,8 @@ enum E with M, N { one, two, three; } '''); var eEnum = library.enums.named('E'); - expect(eEnum.mixedInTypes, hasLength(2)); - expect(eEnum.mixedInTypes.map((i) => i.name), equals(['M', 'N'])); + expect(eEnum.mixedInElements, hasLength(2)); + expect(eEnum.mixedInElements.map((e) => e.name), equals(['M', 'N'])); } void test_namedConstructorCanBeReferenced() async { diff --git a/test/templates/class_test.dart b/test/templates/class_test.dart new file mode 100644 index 0000000000..30dfc30478 --- /dev/null +++ b/test/templates/class_test.dart @@ -0,0 +1,202 @@ +// Copyright (c) 2024, 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 'package:analyzer/file_system/memory_file_system.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../dartdoc_test_base.dart'; +import '../src/test_descriptor_utils.dart' as d; +import '../src/utils.dart'; + +void main() async { + defineReflectiveSuite(() { + defineReflectiveTests(ClassTest); + }); +} + +@reflectiveTest +class ClassTest extends DartdocTestBase { + static const packageName = 'class_test'; + + @override + String get libraryName => 'class'; + + Future createPackage({ + List libFiles = const [], + }) async { + packagePath = await d.createPackage( + packageName, + pubspec: ''' +name: class_test +version: 0.0.1 +environment: + sdk: '>=3.3.0-0 <4.0.0' +''', + dartdocOptions: ''' +dartdoc: + linkToSource: + root: '.' + uriTemplate: 'https://github.com/dart-lang/TEST_PKG/%f%#L%l%' +''', + libFiles: libFiles, + resourceProvider: resourceProvider, + ); + await writeDartdocResources(resourceProvider); + packageConfigProvider.addPackageToConfigFor( + packagePath, packageName, Uri.file('$packagePath/')); + } + + Future> createPackageAndReadLines({ + required List libFiles, + required List filePath, + }) async { + await createPackage(libFiles: libFiles); + await (await buildDartdoc()).generateDocs(); + + return resourceProvider.readLines([packagePath, 'doc', ...filePath]); + } + + void test_class_extends() async { + var baseLines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base {} +class Foo extends Base {} +'''), + ], + filePath: ['lib', 'Base-class.html'], + ); + + baseLines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • Foo
  • '), + matches('
'), + ]); + } + + void test_class_implements() async { + var baseLines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base {} +class Foo implements Base {} +'''), + ], + filePath: ['lib', 'Base-class.html'], + ); + + baseLines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • Foo
  • '), + matches('
'), + ]); + } + + void test_class_implements_withGenericType() async { + var baseLines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base {} +class Foo implements Base {} +'''), + ], + filePath: ['lib', 'Base-class.html'], + ); + + baseLines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • Foo
  • '), + matches('
'), + ]); + } + + void test_class_implements_withInstantiatedType() async { + var baseLines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base {} +class Foo implements Base {} +'''), + ], + filePath: ['lib', 'Base-class.html'], + ); + + baseLines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • Foo
  • '), + matches('
'), + ]); + } + + void test_extensionType_implements() async { + var base1Lines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base1 {} +class Base2 extends Base1 {} +extension type ET(Base2 base) implements Base1 {} +'''), + ], + filePath: ['lib', 'Base1-class.html'], + ); + + base1Lines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • Base2
  • '), + matches('
  • ET
  • '), + matches('
'), + ]); + } + + void test_mixin_implements() async { + var baseLines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base {} +mixin M implements Base {} +'''), + ], + filePath: ['lib', 'Base-class.html'], + ); + + baseLines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • M
  • '), + matches('
'), + ]); + } + + @FailingTest(reason: 'Not implemented yet; should be?') + void test_mixin_superclassConstraint() async { + var baseLines = await createPackageAndReadLines( + libFiles: [ + d.file('lib.dart', ''' +class Base {} +mixin M on Base {} +'''), + ], + filePath: ['lib', 'Base-class.html'], + ); + + baseLines.expectMainContentContainsAllInOrder([ + matches('
Implementers
'), + matches('
    '), + matches('
  • M
  • '), + matches('
'), + ]); + } +} + +extension on MemoryResourceProvider { + List readLines(List pathParts) => + getFile(path.joinAll(pathParts)).readAsStringSync().split('\n'); +} diff --git a/tool/mustachio/codegen_runtime_renderer.dart b/tool/mustachio/codegen_runtime_renderer.dart index 2c409cca93..e8876ebc38 100644 --- a/tool/mustachio/codegen_runtime_renderer.dart +++ b/tool/mustachio/codegen_runtime_renderer.dart @@ -156,13 +156,8 @@ import '${path.basename(_sourceUri.path)}'; /// return type. Getters annotated with `@internal`, `@protected`, /// `@visibleForOverriding`, or `@visibleForTesting` are not valid. void _addPropertyToProcess(PropertyAccessorElement property) { - if (property.isPrivate || property.isStatic || property.isSetter) return; - if (property.hasInternal || - property.hasProtected || - property.hasVisibleForOverriding || - property.hasVisibleForTesting) { - return; - } + if (property.shouldBeOmitted) return; + var type = _relevantTypeFrom(property.type.returnType); if (type == null) return; @@ -475,13 +470,8 @@ class ${renderer._rendererClassName}${renderer._typeParametersString} return; } - if (property.isPrivate || property.isStatic || property.isSetter) return; - if (property.hasInternal || - property.hasProtected || - property.hasVisibleForOverriding || - property.hasVisibleForTesting) { - return; - } + if (property.shouldBeOmitted) return; + _buffer.writeln("'${property.name}': Property("); _buffer .writeln('getValue: ($_contextTypeVariable c) => c.${property.name},'); @@ -717,3 +707,20 @@ extension on InterfaceElement { }; } } + +extension on PropertyAccessorElement { + // Whether this property should be omitted from the runtime renderer code. + bool get shouldBeOmitted { + return isPrivate || + isStatic || + isSetter || + hasInternal || + hasProtected || + hasVisibleForOverriding || + hasVisibleForTesting || + variable.hasInternal || + variable.hasProtected || + variable.hasVisibleForOverriding || + variable.hasVisibleForTesting; + } +}