Files
sdk/pkg/api_summary/lib/src/api_description.dart
T
kevmoo 1e8a9d5d45 [api_summary] Include mixins in textual API summaries
Fixes an issue where with mixin clauses were omitted when generating textual API summaries for class and interface declarations.

Regenerates api.txt for analyzer and analyzer_plugin.

Change-Id: Ic33d76955cefb31709e265ec4ea9d5df9a065f7b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/503701
Auto-Submit: Kevin Moore <kevmoo@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
Commit-Queue: Kevin Moore <kevmoo@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
2026-05-22 11:35:09 -07:00

495 lines
19 KiB
Dart

// Copyright (c) 2026, 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:collection';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'api_summary_customizer.dart';
import 'extensions.dart';
import 'member_sorting.dart';
import 'node.dart';
import 'unique_namer.dart';
import 'uri_sorting.dart';
/// Data structure keeping track of a package's API while walking it to produce
/// `api.txt`.
class ApiDescription {
final ApiSummaryCustomizer _customizer;
final String _pkgName;
/// Top level elements that have already had their child elements dumped.
///
/// If an element is seen again in a different library, it will be followed
/// with `(see above)` (rather than having its child elements dumped twice).
final _dumpedTopLevelElements = <Element>{};
/// Top level elements that have been referenced so far and haven't yet been
/// processed by [build].
///
/// This is used to ensure that all elements referred to by the public API
/// (e.g., by being mentioned in the type of an API element) also show up in
/// the output.
final _potentiallyDanglingReferences = Queue<Element>();
final _uniqueNamer = UniqueNamer();
/// Cache of values returned by [_getOrComputeImmediateSubinterfaceMap], to
/// avoid unnecessary recomputation.
final _immediateSubinterfaceCache =
<LibraryElement, Map<ClassElement, Set<InterfaceElement>>>{};
ApiDescription(this._pkgName, this._customizer);
/// Builds a list of [Node] objects representing all the libraries that are
/// relevant to the package's public API.
///
/// This includes libraries that are in the package's public API as well as
/// libraries that are referenced by the package's public API (either by being
/// re-exported as part of the package's public API, or by being used as part
/// of the type of something in the public API).
///
/// Each library node is pared with a [UriSortKey] indicating the order in
/// which the nodes should be output.
Future<List<(UriSortKey, Node)>> build(AnalysisContext context) async {
_customizer.packageName = _pkgName;
_customizer.analysisContext = context;
await _customizer.setupComplete();
// First, find all the libraries comprising the package's public API, and
// all the top level elements they export.
var publicApiLibraries = <LibraryElement>[];
var topLevelPublicElements = <Element>{};
for (var file in context.contextRoot.analyzedFiles().sorted()) {
if (!file.endsWith('.dart')) continue;
var fileResult = context.currentSession.getFile(file) as FileResult;
var uri = fileResult.uri;
if (fileResult.isLibrary && uri.isInPublicLibOf(_pkgName)) {
var resolvedLibraryResult =
(await context.currentSession.getResolvedLibrary(file))
as ResolvedLibraryResult;
var library = resolvedLibraryResult.element;
topLevelPublicElements.addAll(
library.exportNamespace.definedNames2.values,
);
publicApiLibraries.add(library);
}
}
_customizer.publicApiLibraries = publicApiLibraries;
_customizer.topLevelPublicElements = topLevelPublicElements;
await _customizer.initialScanComplete();
// Then, dump all the libraries in the package's public API.
var nodes = <Uri, Node<MemberSortKey>>{};
for (var library in publicApiLibraries) {
var node = nodes[library.uri] = Node<MemberSortKey>();
_dumpLibrary(library, node);
}
// Finally, dump anything referenced by those public libraries.
while (_potentiallyDanglingReferences.isNotEmpty) {
var element = _potentiallyDanglingReferences.removeFirst();
if (!_dumpedTopLevelElements.add(element)) continue;
var containingLibraryUri = element.library!.uri;
var childNode = Node<MemberSortKey>()
..text.add(_uniqueNamer.name(element));
_dumpElement(element, childNode);
(nodes[containingLibraryUri] ??= Node<MemberSortKey>()
..text.add('$containingLibraryUri:'))
.childNodes
.add((MemberSortKey(element), childNode));
}
return [
for (var entry in nodes.entries)
(UriSortKey(entry.key, _pkgName), entry.value),
];
}
/// Creates a list of objects which, when their string representations are
/// concatenated, describes [type].
///
/// The reason we use this method rather than [DartType.toString] is to make
/// sure that (a) every element mentioned by the type is added to
/// [_potentiallyDanglingReferences], and (b) if an ambiguous name is used,
/// the ambiguity will be taken care of by [_uniqueNamer].
List<Object?> _describeType(DartType type) {
var suffix = switch (type.nullabilitySuffix) {
NullabilitySuffix.none => '',
NullabilitySuffix.star => '*',
NullabilitySuffix.question => '?',
};
switch (type) {
case DynamicType():
return ['dynamic'];
case FunctionType(
:var returnType,
:var typeParameters,
:var formalParameters,
):
var params = <List<Object?>>[];
var optionalParams = <List<Object?>>[];
var namedParams = <String, List<Object?>>{};
for (var formalParameter in formalParameters) {
if (formalParameter.isNamed) {
namedParams[formalParameter.name!] = [
if (formalParameter.isDeprecated) 'deprecated ',
if (formalParameter.isRequired) 'required ',
..._describeType(formalParameter.type),
];
} else if (formalParameter.isOptional) {
optionalParams.add([
if (formalParameter.isDeprecated) 'deprecated ',
..._describeType(formalParameter.type),
]);
} else {
params.add([
if (formalParameter.isDeprecated) 'deprecated ',
..._describeType(formalParameter.type),
]);
}
}
if (optionalParams.isNotEmpty) {
params.add(optionalParams.separatedBy(prefix: '[', suffix: ']'));
}
if (namedParams.isNotEmpty) {
params.add(
namedParams.entries
.sortedBy((e) => e.key)
.map((e) => [...e.value, ' ${e.key}'])
.separatedBy(prefix: '{', suffix: '}'),
);
}
return <Object?>[
..._describeType(returnType),
' Function',
if (typeParameters.isNotEmpty)
...typeParameters
.map(_describeTypeParameter)
.separatedBy(prefix: '<', suffix: '>'),
'(',
...params.separatedBy(),
')',
suffix,
];
case InterfaceType(:var element, :var typeArguments):
_potentiallyDanglingReferences.addLast(element);
return [
_uniqueNamer.name(element),
if (typeArguments.isNotEmpty)
...typeArguments
.map(_describeType)
.separatedBy(prefix: '<', suffix: '>'),
suffix,
];
case RecordType(:var positionalFields, :var namedFields):
if (positionalFields.length == 1 && namedFields.isEmpty) {
return [
'(',
..._describeType(positionalFields[0].type),
',)',
suffix,
];
}
return [
...[
for (var positionalField in positionalFields)
_describeType(positionalField.type),
if (namedFields.isNotEmpty)
namedFields
.sortedBy((f) => f.name)
.map((f) => [..._describeType(f.type), ' ', f.name])
.separatedBy(prefix: '{', suffix: '}'),
].separatedBy(prefix: '(', suffix: ')'),
suffix,
];
case TypeParameterType(:var element):
return [element.name!, suffix];
case VoidType():
return ['void'];
case dynamic(:var runtimeType):
throw UnimplementedError('Unexpected type: $runtimeType');
}
}
/// Creates a list of objects which, when their string representations are
/// concatenated, describes [typeParameter].
List<Object?> _describeTypeParameter(TypeParameterElement typeParameter) {
return [
typeParameter.name!,
if (typeParameter.bound case var bound?) ...[
' extends ',
..._describeType(bound),
],
];
}
/// Appends information to [node] describing [element].
void _dumpElement(Element element, Node<MemberSortKey> node) {
var enclosingElement = element.enclosingElement;
if (enclosingElement is LibraryElement &&
!_customizer.shouldShowDetails(element)) {
if (!enclosingElement.uri.isIn(_pkgName)) {
node.text.add(' (referenced)');
} else {
node.text.add(' (non-public)');
}
return;
}
var parentheticals = <List<Object?>>[];
switch (element) {
case TypeAliasElement(:var aliasedType, :var typeParameters):
var description = <Object?>['type alias'];
if (typeParameters.isNotEmpty) {
description.addAll(
typeParameters
.map(_describeTypeParameter)
.separatedBy(prefix: '<', suffix: '>'),
);
}
description.addAll([' for ', ..._describeType(aliasedType)]);
parentheticals.add(description);
case InstanceElement():
switch (element) {
case InterfaceElement(
:var typeParameters,
:var supertype,
:var mixins,
:var interfaces,
):
var instanceDescription = <Object?>[
switch (element) {
ClassElement() => 'class',
EnumElement() => 'enum',
MixinElement() => 'mixin',
ExtensionTypeElement() => 'extension type',
dynamic(:var runtimeType) => 'TODO: $runtimeType',
},
];
if (typeParameters.isNotEmpty) {
instanceDescription.addAll(
typeParameters
.map(_describeTypeParameter)
.separatedBy(prefix: '<', suffix: '>'),
);
}
if (element is! EnumElement && supertype != null) {
instanceDescription.addAll([
' extends ',
..._describeType(supertype),
]);
}
if (mixins.isNotEmpty) {
instanceDescription.addAll(
mixins.map(_describeType).separatedBy(prefix: ' with '),
);
}
if (element is MixinElement &&
element.superclassConstraints.isNotEmpty) {
instanceDescription.addAll(
element.superclassConstraints
.map(_describeType)
.separatedBy(prefix: ' on '),
);
}
if (interfaces.isNotEmpty) {
instanceDescription.addAll(
interfaces
.map(_describeType)
.separatedBy(prefix: ' implements '),
);
}
parentheticals.add(instanceDescription);
if (element is ClassElement) {
if (element.isSealed) {
var parenthetical = <Object>['sealed'];
parentheticals.add(parenthetical);
if (_getOrComputeImmediateSubinterfaceMap(
element.library,
)[element]
case var subinterfaces?) {
parenthetical.add(' (immediate subtypes: ');
// Note: it's tempting to just do
// `subinterfaces.map(_uniqueNamer.name).join(', ')`, but that
// won't work, because the names returned by
// `UniqueName.toString()` aren't finalized until we've
// visited the entire API and seen if there are class names
// that need to be disambiguated. So we accumulate the
// `UniqueName` objects into the `parenthetical` list and rely
// on `printNodes` converting everything to a string when the
// final API description is being output.
var commaNeeded = false;
for (var subinterface in subinterfaces) {
if (commaNeeded) {
parenthetical.add(', ');
} else {
commaNeeded = true;
}
parenthetical.add(_uniqueNamer.name(subinterface));
}
parenthetical.add(')');
}
} else {
if (element.isAbstract) {
parentheticals.add(['abstract']);
}
if (element.isBase) {
parentheticals.add(['base']);
}
if (element.isMixinClass) {
parentheticals.add(['mixin']);
}
if (element.isInterface) {
parentheticals.add(['interface']);
}
if (element.isFinal) {
parentheticals.add(['final']);
}
}
} else if (element is MixinElement) {
if (element.isBase) {
parentheticals.add(['base']);
}
}
case ExtensionElement(:var extendedType):
parentheticals.add([
'extension on ',
..._describeType(extendedType),
]);
case dynamic(:var runtimeType):
throw UnimplementedError('Unexpected element: $runtimeType');
}
for (var member in element.children.sortedBy((m) => m.name ?? '')) {
if (member.name case var name? when name.startsWith('_')) {
// Ignore private members
continue;
}
if (member is FieldElement) {
// Ignore fields; we care about the getters and setters they induce.
continue;
}
if (member is ConstructorElement &&
element is ClassElement &&
element.isAbstract &&
(element.isFinal || element.isInterface || element.isSealed)) {
// The class can't be constructed from outside of the library that
// declares it, so its constructors aren't part of the public API.
continue;
}
if (member is ConstructorElement && element is EnumElement) {
// Enum constructors can't be called from outside the enum itself,
// so they aren't part of the public API.
continue;
}
var childNode = Node<MemberSortKey>();
childNode.text.add(member.apiName);
_dumpElement(member, childNode);
node.childNodes.add((MemberSortKey(member), childNode));
}
case TopLevelFunctionElement(:var type):
parentheticals.add(['function: ', ..._describeType(type)]);
case ExecutableElement(:var isStatic):
var maybeStatic = isStatic ? 'static ' : '';
switch (element) {
case GetterElement(:var type):
parentheticals.add([
'${maybeStatic}getter: ',
..._describeType(type.returnType),
]);
case SetterElement(:var type):
parentheticals.add([
'${maybeStatic}setter: ',
..._describeType(type.formalParameters.single.type),
]);
case MethodElement(:var type):
parentheticals.add([
'${maybeStatic}method: ',
..._describeType(type),
]);
case ConstructorElement(:var type):
parentheticals.add(['constructor: ', ..._describeType(type)]);
case dynamic(:var runtimeType):
throw UnimplementedError('Unexpected element: $runtimeType');
}
case dynamic(:var runtimeType):
throw UnimplementedError('Unexpected element: $runtimeType');
}
// For synthetic elements such as getters/setters induced by top level
// variables and fields, annotations can be found on the corresponding
// non-synthetic element.
var nonSyntheticElement = element.nonSynthetic;
if (nonSyntheticElement.metadata.hasDeprecated) {
parentheticals.add(['deprecated']);
}
if (nonSyntheticElement.metadata.hasExperimental) {
parentheticals.add(['experimental']);
}
if (parentheticals.isNotEmpty) {
node.text.addAll(parentheticals.separatedBy(prefix: ' (', suffix: ')'));
}
if (node.childNodes.isNotEmpty) {
node.text.add(':');
}
}
/// Appends information to [node] describing [library].
void _dumpLibrary(LibraryElement library, Node<MemberSortKey> node) {
var uri = library.uri;
node.text.addAll([uri, ':']);
var definedNames = library.exportNamespace.definedNames2;
for (var key in definedNames.keys.sorted()) {
var element = definedNames[key]!;
var childNode = Node<MemberSortKey>()
..text.add(_uniqueNamer.name(element));
if (!_dumpedTopLevelElements.add(element)) {
childNode.text.add(' (see above)');
} else {
_dumpElement(element, childNode);
}
node.childNodes.add((MemberSortKey(element), childNode));
}
}
/// Returns a map from each sealed class in [library] to the set of its
/// immediate sub-interfaces.
///
/// If this method has been called before with the same [library], a cached
/// map is returned from [_immediateSubinterfaceCache]. Otherwise a fresh map
/// is computed.
Map<ClassElement, Set<InterfaceElement>>
_getOrComputeImmediateSubinterfaceMap(LibraryElement library) {
if (_immediateSubinterfaceCache[library] case var m?) return m;
var result = <ClassElement, Set<InterfaceElement>>{};
for (var interface in [
...library.classes,
...library.mixins,
...library.enums,
...library.extensionTypes,
]..sortBy((e) => e.name!)) {
for (var superinterface in [
interface.supertype,
...interface.interfaces,
...interface.mixins,
if (interface is MixinElement) ...interface.superclassConstraints,
]) {
if (superinterface == null) continue;
var superinterfaceElement = superinterface.element;
if (superinterfaceElement is ClassElement &&
superinterfaceElement.isSealed) {
(result[superinterfaceElement] ??= {}).add(interface);
}
}
}
_immediateSubinterfaceCache[library] = result;
return result;
}
}