From 199c7b4ab961efb6692af0a1e30981250dd44ad0 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 30 Aug 2024 08:54:02 -0700 Subject: [PATCH] Display indirectly implemented types via extended and mixed-in types (#3855) --- lib/src/model/inheriting_container.dart | 58 ++++++++++++------- test/classes_test.dart | 77 ++++++++++++++++++++++--- test/src/utils.dart | 33 +++++------ test/templates/class_test.dart | 51 ++++++++++++---- 4 files changed, 160 insertions(+), 59 deletions(-) diff --git a/lib/src/model/inheriting_container.dart b/lib/src/model/inheriting_container.dart index bd30943e16..4fc64ce8b5 100644 --- a/lib/src/model/inheriting_container.dart +++ b/lib/src/model/inheriting_container.dart @@ -453,38 +453,52 @@ abstract class InheritingContainer extends Container { } } - for (var interface in directInterfaces) { - var interfaceElement = interface.modelElement; + void addFromSupertype(DefinedElementType supertype, + {required bool addSupertypes}) { + var superElement = supertype.modelElement; /// Do not recurse if we can find an element here. - if (interfaceElement.canonicalModelElement != null) { - addInterfaceIfUnique(interface); - continue; + if (superElement.canonicalModelElement != null) { + if (addSupertypes) addInterfaceIfUnique(supertype); + return; } - // Public types used to be unconditionally exposed here. However, - // if the packages are [DocumentLocation.missing] we generally treat types - // defined in them as actually defined in a documented package. - // That translates to them being defined here, but in 'src/' or similar, - // and so, are now always hidden. - - // This type is not backed by a canonical Class; search - // the superchain and publicInterfaces of this interface to pretend - // as though the hidden class didn't exist and this class was declared - // directly referencing the canonical classes further up the chain. - if (interfaceElement is! InheritingContainer) { + + // This type is not backed by a canonical Class; it is not documented. + // Search it's `superChain` and `publicInterfaces` to pretend that `this` + // container directly implements canonical classes further up the chain. + + if (superElement is! InheritingContainer) { assert( false, - 'Can not handle intermediate non-public interfaces created by ' + 'Cannot handle intermediate non-public interfaces created by ' "ModelElements that are not classes or mixins: '$fullyQualifiedName' " - "contains an interface '$interface', defined by '$interfaceElement'", + "contains a supertype '$supertype', defined by '$superElement'", ); - continue; + return; } - var publicSuperChain = interfaceElement.superChain.wherePublic; - if (publicSuperChain.isNotEmpty) { + var publicSuperChain = superElement.superChain.wherePublic; + if (publicSuperChain.isNotEmpty && addSupertypes) { addInterfaceIfUnique(publicSuperChain.first); } - interfaceElement.publicInterfaces.forEach(addInterfaceIfUnique); + superElement.publicInterfaces.forEach(addInterfaceIfUnique); + } + + for (var interface in directInterfaces) { + addFromSupertype(interface, addSupertypes: true); + } + for (var supertype in superChain) { + var interfaceElement = supertype.modelElement; + + // Do not recurse if we can find an element here. + if (interfaceElement.canonicalModelElement != null) { + continue; + } + addFromSupertype(supertype, addSupertypes: false); + } + if (this case Class(:var mixedInTypes) || Enum(:var mixedInTypes)) { + for (var mixin in mixedInTypes) { + addFromSupertype(mixin, addSupertypes: false); + } } return interfaces; } diff --git a/test/classes_test.dart b/test/classes_test.dart index 6b33d307c5..584e24b627 100644 --- a/test/classes_test.dart +++ b/test/classes_test.dart @@ -25,9 +25,9 @@ class C {} extension Ex on C {} '''); - var f = library.classes.named('C'); + var c = library.classes.named('C'); expect( - f.potentiallyApplicableExtensionsSorted, + c.potentiallyApplicableExtensionsSorted, contains(library.extensions.named('Ex')), ); } @@ -38,9 +38,9 @@ class C {} extension Ex on C {} '''); - var f = library.classes.named('C'); + var c = library.classes.named('C'); expect( - f.potentiallyApplicableExtensionsSorted, + c.potentiallyApplicableExtensionsSorted, contains(library.extensions.named('Ex')), ); } @@ -52,9 +52,9 @@ class D extends C {} extension Ex on C {} '''); - var f = library.classes.named('D'); + var d = library.classes.named('D'); expect( - f.potentiallyApplicableExtensionsSorted, + d.potentiallyApplicableExtensionsSorted, contains(library.extensions.named('Ex')), ); } @@ -66,9 +66,9 @@ class D extends C {} extension Ex on C {} '''); - var f = library.classes.named('D'); + var d = library.classes.named('D'); expect( - f.potentiallyApplicableExtensionsSorted, + d.potentiallyApplicableExtensionsSorted, contains(library.extensions.named('Ex')), ); } @@ -109,6 +109,67 @@ class C implements _B {} expect(c.publicInterfaces.first.modelElement, library.classes.named('A')); } + void test_publicInterfaces_indirectViaPrivate2() async { + var library = await bootPackageWithLibrary(''' +class A {} +class _B implements A {} +class _C implements _B {} +class D implements _C {} +'''); + + var d = library.classes.named('D'); + expect(d.publicInterfaces, hasLength(1)); + expect(d.publicInterfaces.first.modelElement, library.classes.named('A')); + } + + void test_publicInterfaces_indirectViaPrivateExtendedClass() async { + var library = await bootPackageWithLibrary(''' +class A {} +class _B implements A {} +class C extends _B {} +'''); + + var c = library.classes.named('C'); + expect(c.publicInterfaces, hasLength(1)); + expect(c.publicInterfaces.first.modelElement, library.classes.named('A')); + } + + void test_publicInterfaces_indirectViaPrivateExtendedClass2() async { + var library = await bootPackageWithLibrary(''' +class A {} +class _B implements A {} +class _C extends _B {} +class D extends _C {} +'''); + + var c = library.classes.named('D'); + expect(c.publicInterfaces, hasLength(1)); + expect(c.publicInterfaces.first.modelElement, library.classes.named('A')); + } + + void test_publicInterfaces_onlyExtendedClasses() async { + var library = await bootPackageWithLibrary(''' +class A {} +class B extends A {} +class _C extends B {} +class D extends _C {} +'''); + + expect(library.classes.named('D').publicInterfaces, isEmpty); + } + + void test_publicInterfaces_indirectViaPrivateMixedInMixin() async { + var library = await bootPackageWithLibrary(''' +class A {} +mixin _M implements A {} +class C with _M {} +'''); + + var c = library.classes.named('C'); + expect(c.publicInterfaces, hasLength(1)); + expect(c.publicInterfaces.first.modelElement, library.classes.named('A')); + } + void test_publicInterfaces_indirectViaPrivate_multiply() async { var library = await bootPackageWithLibrary(''' class A {} diff --git a/test/src/utils.dart b/test/src/utils.dart index 7028b0d553..97dea526a4 100644 --- a/test/src/utils.dart +++ b/test/src/utils.dart @@ -322,35 +322,34 @@ MatchingLinkResult referenceLookup(Warnable element, String codeRef) => /// Returns a matcher which compresses consecutive whitespace in [text] into a /// single space. -Matcher matchesCompressed(String text) => matches(RegExp(text.replaceAll( - RegExp(r'\s\s+', multiLine: true), - ' *', - ))); - -/// We can not use [ExperimentalFeature.releaseVersion] or even -/// [ExperimentalFeature.experimentalReleaseVersion] as these are set to `null` -/// even when partial analyzer implementations are available. +Matcher matchesCompressed(String text) => matches(RegExp( + text.replaceAll(RegExp(r'\s\s+', multiLine: true), r'\s*'), + )); + +// We can not use `ExperimentalFeature.releaseVersion` or even +// `ExperimentalFeature.experimentalReleaseVersion` as these are set to `null` +// even when partial analyzer implementations are available. bool get namedArgumentsAnywhereAllowed => VersionRange(min: Version.parse('2.17.0-0'), includeMin: true) .allows(platformVersion); -/// We can not use [ExperimentalFeature.releaseVersion] or even -/// [ExperimentalFeature.experimentalReleaseVersion] as these are set to `null` -/// even when partial analyzer implementations are available. +// We can not use `ExperimentalFeature.releaseVersion` or even +// `ExperimentalFeature.experimentalReleaseVersion` as these are set to `null` +// even when partial analyzer implementations are available. bool get recordsAllowed => VersionRange(min: Version.parse('2.19.0-0'), includeMin: true) .allows(platformVersion); -/// We can not use [ExperimentalFeature.releaseVersion] or even -/// [ExperimentalFeature.experimentalReleaseVersion] as these are set to `null` -/// even when partial analyzer implementations are available. +// We can not use `ExperimentalFeature.releaseVersion` or even +// `ExperimentalFeature.experimentalReleaseVersion` as these are set to `null` +// even when partial analyzer implementations are available. bool get extensionTypesAllowed => VersionRange(min: Version.parse('3.2.0-0.0-dev'), includeMin: true) .allows(platformVersion); -/// We can not use [ExperimentalFeature.releaseVersion] or even -/// [ExperimentalFeature.experimentalReleaseVersion] as these are set to `null` -/// even when partial analyzer implementations are available. +// We can not use `ExperimentalFeature.releaseVersion` or even +// `ExperimentalFeature.experimentalReleaseVersion` as these are set to `null` +// even when partial analyzer implementations are available. bool get classModifiersAllowed => VersionRange(min: Version.parse('3.0.0-0.0-dev'), includeMin: true) .allows(platformVersion); diff --git a/test/templates/class_test.dart b/test/templates/class_test.dart index f57dd6c772..10a76c2aac 100644 --- a/test/templates/class_test.dart +++ b/test/templates/class_test.dart @@ -46,12 +46,36 @@ class Foo implements _Foo {} '''); var baseLines = readLines(['lib', 'Base-class.html']); - baseLines.expectMainContentContainsAllInOrder([ - matches('
Implementers
'), - matches('
    '), - matches('
  • Foo
  • '), - matches('
'), - ]); + expect( + baseLines.join('\n'), + matchesCompressed(''' +
Implementers
+
+'''), + ); + } + + void test_implementers_class_extends2() async { + await createPackageWithLibrary(''' +abstract class A {} +abstract class B extends A {} +class _C extends B {} +class D extends _C {} +'''); + var baseLines = readLines(['lib', 'A-class.html']); + + expect( + baseLines.join('\n'), + // D should not be found; it is indirect via B. + matchesCompressed(''' +
Implementers
+
    +
  • B
  • +
+'''), + ); } void test_implementers_class_implements_withGenericType() async { @@ -61,12 +85,15 @@ class Foo implements Base {} '''); var baseLines = readLines(['lib', 'Base-class.html']); - baseLines.expectMainContentContainsAllInOrder([ - matches('
Implementers
'), - matches('
    '), - matches('
  • Foo
  • '), - matches('
'), - ]); + expect( + baseLines.join('\n'), + matchesCompressed(r''' +
Implementers
+
+'''), + ); } void test_implementers_class_implements_withInstantiatedType() async {