| // 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 'package:collection/collection.dart'; | |
| import 'api_declaration.dart'; | |
| import 'api_type.dart'; | |
| import 'extensions.dart'; | |
| import 'member_sorting.dart'; | |
| import 'node.dart'; | |
| import 'uri_sorting.dart'; | |
| /// Converts the [package] model into a canonical, human-readable textual | |
| /// summary for `api.txt`. | |
| String renderTextSummary(ApiSummary package) => | |
| _ApiTextRenderer(package).render(); | |
| /// Represents a unique name with an optional numerical disambiguator suffix. | |
| final class _JsonUniqueName { | |
| final String _nameHint; | |
| int? _disambiguator; | |
| _JsonUniqueName(_JsonUniqueNamer uniqueNamer, this._nameHint) | |
| : assert(!_nameHint.contains('@')) { | |
| final conflicts = uniqueNamer._conflicts[_nameHint] ??= []; | |
| if (conflicts.length == 1) { | |
| conflicts[0]._disambiguator = 1; | |
| } | |
| conflicts.add(this); | |
| if (conflicts.length > 1) { | |
| _disambiguator = conflicts.length; | |
| } | |
| } | |
| @override | |
| String toString() => [ | |
| _nameHint, | |
| if (_disambiguator case final disambiguator?) '@$disambiguator', | |
| ].join(); | |
| } | |
| /// Tracks name hints and handles collisions by assigning disambiguation | |
| /// suffixes. | |
| final class _JsonUniqueNamer { | |
| final _names = <String, _JsonUniqueName>{}; | |
| final _conflicts = <String, List<_JsonUniqueName>>{}; | |
| _JsonUniqueName name(String elementKey, String nameHint) => | |
| _names[elementKey] ??= _JsonUniqueName(this, nameHint); | |
| } | |
| enum _Kind { classDeclaration, enumDeclaration, mixinDeclaration } | |
| /// Internal renderer converting structured summary models into canonical text. | |
| final class _ApiTextRenderer { | |
| final ApiSummary _package; | |
| final _uniqueNamer = _JsonUniqueNamer(); | |
| final _renderedTopLevelElements = | |
| <String>{}; // unique key: "locationUri#name" | |
| final _potentiallyDanglingReferences = <ApiInterfaceType>[]; | |
| final _reexportedExternalElements = | |
| < | |
| ({ | |
| Uri locationUri, | |
| MemberSortKey sortKey, | |
| _JsonUniqueName name, | |
| String key, | |
| }) | |
| >[]; | |
| _ApiTextRenderer(this._package); | |
| String? _externalDependencyConstraint(Uri uri) => switch (uri.packageName) { | |
| final pkg? when pkg != _package.name => _package.dependencies[pkg], | |
| _ => null, | |
| }; | |
| Node<MemberSortKey> _createDanglingLibraryNode(Uri uri) { | |
| final node = Node<MemberSortKey>()..text.add(uri); | |
| if (_externalDependencyConstraint(uri) case final constraint?) { | |
| node.text.add(' ($constraint)'); | |
| } | |
| node.text.add(':'); | |
| return node; | |
| } | |
| String render() { | |
| final nodes = <Uri, Node<MemberSortKey>>{}; | |
| final experimentalUris = <Uri>{}; | |
| final sortedLibraries = _package.libraries.sortedByUri(_package.name); | |
| for (final library in sortedLibraries) { | |
| final uri = Uri.parse(library.uri); | |
| if (library.isExperimental) { | |
| experimentalUris.add(uri); | |
| } | |
| final node = nodes[uri] = Node<MemberSortKey>(); | |
| _renderLibrary(library, node); | |
| } | |
| // Process dangling references | |
| var i = 0; | |
| while (i < _potentiallyDanglingReferences.length) { | |
| final type = _potentiallyDanglingReferences[i]; | |
| i++; | |
| if (type.libraryUri == null) continue; | |
| final key = '${type.libraryUri}#${type.name}'; | |
| if (!_renderedTopLevelElements.add(key)) continue; | |
| final containingLibraryUri = Uri.parse(type.libraryUri!); | |
| final childNode = Node<MemberSortKey>() | |
| ..text.add(_uniqueNamer.name(key, type.name)); | |
| // Since it's a dangling reference, it is either referenced or non-public | |
| if (!containingLibraryUri.isIn(_package.name)) { | |
| childNode.text.add(' (referenced)'); | |
| } else { | |
| childNode.text.add(' (non-public)'); | |
| } | |
| (nodes[containingLibraryUri] ??= _createDanglingLibraryNode( | |
| containingLibraryUri, | |
| )).childNodes.add((_computeSortKeyForDangling(type), childNode)); | |
| } | |
| final renderedExternalSeeAbove = <String>{}; | |
| for (final reexport in _reexportedExternalElements) { | |
| if (!renderedExternalSeeAbove.add( | |
| '${reexport.locationUri}#${reexport.key}', | |
| )) { | |
| continue; | |
| } | |
| final libraryNode = nodes[reexport.locationUri] ??= | |
| _createDanglingLibraryNode(reexport.locationUri); | |
| final childNode = Node<MemberSortKey>() | |
| ..text.add(reexport.name) | |
| ..text.add(' (see above)'); | |
| libraryNode.childNodes.add((reexport.sortKey, childNode)); | |
| } | |
| final sortedNodes = [ | |
| for (final entry in nodes.entries) | |
| ( | |
| UriSortKey( | |
| entry.key, | |
| _package.name, | |
| isExperimental: experimentalUris.contains(entry.key), | |
| ), | |
| entry.value, | |
| ), | |
| ]; | |
| final stringBuffer = StringBuffer(); | |
| if (_package.environment.isNotEmpty) { | |
| stringBuffer.writeln('environment:'); | |
| for (final entry in _package.environment.entries) { | |
| stringBuffer.writeln(' ${entry.key}: ${entry.value}'); | |
| } | |
| } | |
| if (_package.executables.isNotEmpty) { | |
| stringBuffer.writeln('executables:'); | |
| for (final entry in _package.executables.entries) { | |
| if (entry.value != null && entry.value != entry.key) { | |
| stringBuffer.writeln(' ${entry.key}: ${entry.value}'); | |
| } else { | |
| stringBuffer.writeln(' ${entry.key}'); | |
| } | |
| } | |
| } | |
| printNodes(stringBuffer, sortedNodes); | |
| return stringBuffer.toString(); | |
| } | |
| void _renderLibrary(ApiLibrary library, Node<MemberSortKey> node) { | |
| final uri = Uri.parse(library.uri); | |
| node.text.add(uri); | |
| final depConstraint = _externalDependencyConstraint(uri); | |
| final parentheticals = <List<Object?>>[ | |
| if (depConstraint != null) [depConstraint], | |
| for (final facet in library.facets) | |
| for (final segment in facet.parentheticalSegments) [segment], | |
| ]; | |
| if (parentheticals.isNotEmpty) { | |
| node.text.addAll(parentheticals.separatedBy(prefix: ' (', suffix: ')')); | |
| } | |
| node.text.add(':'); | |
| // Combine all top level items to sort them correctly | |
| final allItems = | |
| <({MemberSortKey sortKey, ApiDeclaration element, _Kind? kind})>[]; | |
| for (final e in library.classes) { | |
| allItems.add(( | |
| sortKey: _computeSortKey(e), | |
| element: e, | |
| kind: _Kind.classDeclaration, | |
| )); | |
| } | |
| for (final e in library.enums) { | |
| allItems.add(( | |
| sortKey: _computeSortKey(e), | |
| element: e, | |
| kind: _Kind.enumDeclaration, | |
| )); | |
| } | |
| for (final e in library.mixins) { | |
| allItems.add(( | |
| sortKey: _computeSortKey(e), | |
| element: e, | |
| kind: _Kind.mixinDeclaration, | |
| )); | |
| } | |
| for (final e in library.extensions) { | |
| allItems.add((sortKey: _computeSortKey(e), element: e, kind: null)); | |
| } | |
| for (final e in library.extensionTypes) { | |
| allItems.add((sortKey: _computeSortKey(e), element: e, kind: null)); | |
| } | |
| for (final e in library.functions) { | |
| allItems.add(( | |
| sortKey: _computeSortKey(e, isTopLevel: true), | |
| element: e, | |
| kind: null, | |
| )); | |
| } | |
| for (final e in library.typeAliases) { | |
| allItems.add((sortKey: _computeSortKey(e), element: e, kind: null)); | |
| } | |
| allItems.sort((a, b) => a.sortKey.compareTo(b.sortKey)); | |
| for (final item in allItems) { | |
| final element = item.element; | |
| final name = element.name; | |
| final locationUri = element.locationUri; | |
| var key = '$locationUri#$name'; | |
| if (element is ApiExtension && name.isEmpty) { | |
| final memberNames = element.methods.map((m) => m.name).join(','); | |
| key = '$key#${element.extendedType.toJson()}#$memberNames'; | |
| } | |
| final uniqueName = _uniqueNamer.name(key, name); | |
| final childNode = Node<MemberSortKey>()..text.add(uniqueName); | |
| if (locationUri != null && locationUri != library.uri) { | |
| final parsedLocation = Uri.parse(locationUri); | |
| if (!parsedLocation.isIn(_package.name)) { | |
| _reexportedExternalElements.add(( | |
| locationUri: parsedLocation, | |
| sortKey: item.sortKey, | |
| name: uniqueName, | |
| key: key, | |
| )); | |
| } | |
| } | |
| if (!_renderedTopLevelElements.add(key)) { | |
| childNode.text.add(' (see above)'); | |
| } else { | |
| switch (element) { | |
| case ApiClass(): | |
| _renderClass(library.uri, element, childNode, kind: item.kind!); | |
| case ApiExtension(): | |
| _renderExtension(library.uri, element, childNode); | |
| case ApiExtensionType(): | |
| _renderExtensionType(library.uri, element, childNode); | |
| case ApiExecutable(): | |
| _renderExecutable( | |
| library.uri, | |
| element, | |
| childNode, | |
| isTopLevel: true, | |
| ); | |
| case ApiTypeAlias(): | |
| _renderTypeAlias(library.uri, element, childNode); | |
| } | |
| } | |
| node.childNodes.add((item.sortKey, childNode)); | |
| } | |
| } | |
| void _renderClass( | |
| String libraryUri, | |
| ApiClass element, | |
| Node<MemberSortKey> node, { | |
| required _Kind kind, | |
| }) { | |
| final parentheticals = <List<Object?>>[]; | |
| final instanceDescription = <Object?>[]; | |
| // Determine type | |
| if (kind == _Kind.enumDeclaration) { | |
| instanceDescription.add('enum'); | |
| } else if (element.modifiers.contains(ApiClassModifier.isMixin) && | |
| kind == _Kind.classDeclaration) { | |
| instanceDescription.add('mixin class'); | |
| } else if (element.modifiers.contains(ApiClassModifier.isMixin) || | |
| kind == _Kind.mixinDeclaration) { | |
| instanceDescription.add('mixin'); | |
| } else { | |
| instanceDescription.add('class'); | |
| } | |
| if (element.typeParameters.isNotEmpty) { | |
| instanceDescription.addAll(_renderTypeParameters(element.typeParameters)); | |
| } | |
| if (element.supertype != null) { | |
| instanceDescription.addAll([ | |
| ' extends ', | |
| ..._describeType(element.supertype!), | |
| ]); | |
| } | |
| if (element.superclassConstraints.isNotEmpty) { | |
| instanceDescription.addAll( | |
| element.superclassConstraints | |
| .map(_describeType) | |
| .separatedBy(prefix: ' on '), | |
| ); | |
| } | |
| if (element.mixins.isNotEmpty) { | |
| instanceDescription.addAll( | |
| element.mixins.map(_describeType).separatedBy(prefix: ' with '), | |
| ); | |
| } | |
| if (element.interfaces.isNotEmpty) { | |
| instanceDescription.addAll( | |
| element.interfaces | |
| .map(_describeType) | |
| .separatedBy(prefix: ' implements '), | |
| ); | |
| } | |
| parentheticals.add(instanceDescription); | |
| if (element.modifiers.contains(ApiClassModifier.isSealed)) { | |
| final parenthetical = <Object?>['sealed']; | |
| parentheticals.add(parenthetical); | |
| if (element.immediateSubtypes.isNotEmpty) { | |
| parenthetical.add(' (immediate subtypes: '); | |
| var commaNeeded = false; | |
| for (final sub in element.immediateSubtypes) { | |
| if (commaNeeded) { | |
| parenthetical.add(', '); | |
| } else { | |
| commaNeeded = true; | |
| } | |
| parenthetical.add(sub); | |
| } | |
| parenthetical.add(')'); | |
| } | |
| } else if (kind != _Kind.enumDeclaration) { | |
| if (element.modifiers.contains(ApiClassModifier.isAbstract)) { | |
| parentheticals.add(['abstract']); | |
| } | |
| if (element.modifiers.contains(ApiClassModifier.isBase)) { | |
| parentheticals.add(['base']); | |
| } | |
| if (element.modifiers.contains(ApiClassModifier.isMixin)) { | |
| parentheticals.add(['mixin']); | |
| } | |
| if (element.modifiers.contains(ApiClassModifier.isInterface)) { | |
| parentheticals.add(['interface']); | |
| } | |
| if (element.modifiers.contains(ApiClassModifier.isFinal)) { | |
| parentheticals.add(['final']); | |
| } | |
| } | |
| if (element.status == ApiDeclarationStatus.referenced) { | |
| parentheticals.add(['referenced']); | |
| } else if (element.status == ApiDeclarationStatus.nonPublic) { | |
| parentheticals.add(['non-public']); | |
| } | |
| _renderParentheticals( | |
| parentheticals: parentheticals, | |
| element: element, | |
| node: node, | |
| ); | |
| _renderMembers(element.constructors, element.methods, node); | |
| } | |
| void _renderExtension( | |
| String libraryUri, | |
| ApiExtension element, | |
| Node<MemberSortKey> node, | |
| ) { | |
| final parentheticals = <List<Object?>>[]; | |
| // Call _describeType for its side effect of registering dangling | |
| // references. | |
| for (final bound in element.typeParameters.values) { | |
| if (bound != null) _describeType(bound); | |
| } | |
| parentheticals.add([ | |
| 'extension on ', | |
| ..._describeType(element.extendedType), | |
| ]); | |
| _renderParentheticals( | |
| parentheticals: parentheticals, | |
| element: element, | |
| node: node, | |
| ); | |
| _renderMembers([], element.methods, node); | |
| } | |
| void _renderExtensionType( | |
| String libraryUri, | |
| ApiExtensionType element, | |
| Node<MemberSortKey> node, | |
| ) { | |
| final parentheticals = <List<Object?>>[]; | |
| final instanceDescription = <Object?>['extension type']; | |
| if (element.typeParameters.isNotEmpty) { | |
| instanceDescription.addAll(_renderTypeParameters(element.typeParameters)); | |
| } | |
| if (element.interfaces.isNotEmpty) { | |
| instanceDescription.addAll( | |
| element.interfaces | |
| .map(_describeType) | |
| .separatedBy(prefix: ' implements '), | |
| ); | |
| } | |
| parentheticals.add(instanceDescription); | |
| _renderParentheticals( | |
| parentheticals: parentheticals, | |
| element: element, | |
| node: node, | |
| ); | |
| _renderMembers(element.constructors, element.methods, node); | |
| } | |
| void _renderExecutable( | |
| String libraryUri, | |
| ApiExecutable element, | |
| Node<MemberSortKey> node, { | |
| bool isTopLevel = false, | |
| }) { | |
| final parentheticals = <List<Object?>>[]; | |
| for (final bound in element.typeParameters.values) { | |
| if (bound != null) _describeType(bound); | |
| } | |
| final maybeStatic = | |
| ((element.isStatic || isTopLevel) && | |
| element.kind != ApiExecutableKind.function) | |
| ? 'static ' | |
| : ''; | |
| switch (element.kind) { | |
| case ApiExecutableKind.getter: | |
| if (element.isEnumConstant) { | |
| parentheticals.add([ | |
| 'enum constant: ', | |
| ..._describeType(element.returnType), | |
| ]); | |
| } else { | |
| parentheticals.add([ | |
| '$maybeStatic${element.isConst ? 'const getter: ' : 'getter: '}', | |
| ..._describeType(element.returnType), | |
| ]); | |
| } | |
| case ApiExecutableKind.setter: | |
| parentheticals.add([ | |
| '$maybeStatic${'setter: '}', | |
| ..._describeType(element.parameters.single.type), | |
| ]); | |
| case ApiExecutableKind.method || ApiExecutableKind.function: | |
| parentheticals.add([ | |
| '$maybeStatic${element.kind.name}: ', | |
| ..._describeFunctionLike(element), | |
| ]); | |
| case ApiExecutableKind.constructor: | |
| parentheticals.add([ | |
| element.isConst ? 'const constructor: ' : 'constructor: ', | |
| ..._describeFunctionLike(element), | |
| ]); | |
| } | |
| _renderParentheticals( | |
| parentheticals: parentheticals, | |
| element: element, | |
| node: node, | |
| ); | |
| } | |
| void _renderTypeAlias( | |
| String libraryUri, | |
| ApiTypeAlias element, | |
| Node<MemberSortKey> node, | |
| ) { | |
| final parentheticals = <List<Object?>>[]; | |
| final description = <Object?>['type alias']; | |
| if (element.typeParameters.isNotEmpty) { | |
| description.addAll(_renderTypeParameters(element.typeParameters)); | |
| } | |
| description.addAll([' for ', ..._describeType(element.aliasedType)]); | |
| parentheticals.add(description); | |
| _renderParentheticals( | |
| parentheticals: parentheticals, | |
| element: element, | |
| node: node, | |
| ); | |
| } | |
| void _renderMembers( | |
| List<ApiExecutable> constructors, | |
| List<ApiExecutable> methods, | |
| Node<MemberSortKey> node, | |
| ) { | |
| final members = <({MemberSortKey sortKey, ApiExecutable element})>[]; | |
| for (final e in constructors) { | |
| members.add((sortKey: _computeSortKey(e, isInstance: true), element: e)); | |
| } | |
| for (final e in methods) { | |
| members.add((sortKey: _computeSortKey(e, isInstance: true), element: e)); | |
| } | |
| members.sort((a, b) => a.sortKey.compareTo(b.sortKey)); | |
| if (members.isNotEmpty) { | |
| node.text.add(':'); | |
| } | |
| for (final item in members) { | |
| final member = item.element; | |
| final childNode = Node<MemberSortKey>(); | |
| childNode.text.add(member.name); | |
| _renderExecutable('', member, childNode); | |
| node.childNodes.add((item.sortKey, childNode)); | |
| } | |
| } | |
| List<Object?> _describeFunctionLike(ApiExecutable element) { | |
| final params = <List<Object?>>[]; | |
| final optionalParams = <List<Object?>>[]; | |
| final namedParams = <String, List<Object?>>{}; | |
| for (final param in element.parameters) { | |
| if (param.isNamed) { | |
| namedParams[param.name] = <Object?>[ | |
| if (param.isDeprecated) 'deprecated ', | |
| if (param.isRequired) 'required ', | |
| ..._describeType(param.type), | |
| ]; | |
| } else if (param.isOptionalPositional) { | |
| optionalParams.add(<Object?>[ | |
| if (param.isDeprecated) 'deprecated ', | |
| ..._describeType(param.type), | |
| ]); | |
| } else { | |
| params.add(<Object?>[ | |
| if (param.isDeprecated) 'deprecated ', | |
| ..._describeType(param.type), | |
| ]); | |
| } | |
| } | |
| if (optionalParams.isNotEmpty) { | |
| params.add(optionalParams.separatedBy(prefix: '[', suffix: ']')); | |
| } | |
| if (namedParams.isNotEmpty) { | |
| params.add( | |
| namedParams.entries | |
| .sortedBy((e) => e.key) | |
| .map((e) => <Object?>[...e.value, ' ${e.key}']) | |
| .separatedBy(prefix: '{', suffix: '}'), | |
| ); | |
| } | |
| return <Object?>[ | |
| ..._describeType(element.returnType), | |
| ' Function', | |
| if (element.typeParameters.isNotEmpty) | |
| ..._renderTypeParameters(element.typeParameters), | |
| '(', | |
| ...params.separatedBy(), | |
| ')', | |
| ]; | |
| } | |
| List<Object?> _describeType(ApiType type) { | |
| switch (type) { | |
| case ApiDynamicType(): | |
| return ['dynamic']; | |
| case ApiVoidType(): | |
| return ['void']; | |
| case ApiTypeParameterType(): | |
| return [type.name, if (type.isNullable) '?']; | |
| case ApiInterfaceType(): | |
| _potentiallyDanglingReferences.add(type); | |
| final key = '${type.libraryUri}#${type.name}'; | |
| final name = type.libraryUri != null | |
| ? _uniqueNamer.name(key, type.name) | |
| : type.name; | |
| return [ | |
| name, | |
| if (type.typeArguments.isNotEmpty) | |
| ...type.typeArguments | |
| .map(_describeType) | |
| .separatedBy(prefix: '<', suffix: '>'), | |
| if (type.isNullable) '?', | |
| ]; | |
| case ApiRecordType(): | |
| if (type.positionalFields.length == 1 && type.namedFields.isEmpty) { | |
| return [ | |
| '(', | |
| ..._describeType(type.positionalFields[0]), | |
| ',)', | |
| if (type.isNullable) '?', | |
| ]; | |
| } | |
| return [ | |
| ...<List<Object?>>[ | |
| for (final positionalField in type.positionalFields) | |
| _describeType(positionalField), | |
| if (type.namedFields.isNotEmpty) | |
| type.namedFields | |
| .sortedBy((f) => f.name) | |
| .map((f) => <Object?>[..._describeType(f.type), ' ', f.name]) | |
| .separatedBy(prefix: '{', suffix: '}'), | |
| ].separatedBy(prefix: '(', suffix: ')'), | |
| if (type.isNullable) '?', | |
| ]; | |
| case ApiFunctionType(): | |
| for (final bound in type.typeParameters.values) { | |
| if (bound != null) _describeType(bound); | |
| } | |
| final params = <List<Object?>>[]; | |
| final optionalParams = <List<Object?>>[]; | |
| final namedParams = <String, List<Object?>>{}; | |
| for (final formalParameter in type.parameters) { | |
| if (formalParameter.isNamed) { | |
| namedParams[formalParameter.name] = <Object?>[ | |
| if (formalParameter.isDeprecated) 'deprecated ', | |
| if (formalParameter.isRequired) 'required ', | |
| ..._describeType(formalParameter.type), | |
| ]; | |
| } else if (formalParameter.isOptionalPositional) { | |
| optionalParams.add(<Object?>[ | |
| if (formalParameter.isDeprecated) 'deprecated ', | |
| ..._describeType(formalParameter.type), | |
| ]); | |
| } else { | |
| params.add(<Object?>[ | |
| 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) => <Object?>[...e.value, ' ${e.key}']) | |
| .separatedBy(prefix: '{', suffix: '}'), | |
| ); | |
| } | |
| return <Object?>[ | |
| ..._describeType(type.returnType), | |
| ' Function', | |
| if (type.typeParameters.isNotEmpty) | |
| ..._renderTypeParameters(type.typeParameters), | |
| '(', | |
| ...params.separatedBy(), | |
| ')', | |
| if (type.isNullable) '?', | |
| ]; | |
| } | |
| } | |
| /// Renders declaration metadata flags (deprecation, visibility, meta | |
| /// annotations) into [parentheticals]. | |
| /// | |
| /// Each inner list in [parentheticals] represents an individual | |
| /// comma-separated segment formatted via `separatedBy` (e.g. | |
| /// `(class extends Object, protected, internal)`). | |
| void _renderParentheticals({ | |
| required List<List<Object?>> parentheticals, | |
| required ApiDeclaration element, | |
| required Node<MemberSortKey> node, | |
| }) { | |
| if (element.isDeprecated) { | |
| parentheticals.add(['deprecated']); | |
| } | |
| for (final facet in element.facets) { | |
| for (final segment in facet.parentheticalSegments) { | |
| parentheticals.add([segment]); | |
| } | |
| } | |
| if (parentheticals.isNotEmpty) { | |
| node.text.addAll(parentheticals.separatedBy(prefix: ' (', suffix: ')')); | |
| } | |
| } | |
| List<Object?> _renderTypeParameters(Map<String, ApiType?> typeParameters) { | |
| if (typeParameters.isEmpty) return const []; | |
| return typeParameters.entries | |
| .map( | |
| (e) => <Object?>[ | |
| e.key, | |
| if (e.value != null) ...[' extends ', ..._describeType(e.value!)], | |
| ], | |
| ) | |
| .separatedBy(prefix: '<', suffix: '>'); | |
| } | |
| } | |
| MemberSortKey _computeSortKey( | |
| ApiDeclaration element, { | |
| bool isInstance = false, | |
| bool isTopLevel = false, | |
| }) => switch (element) { | |
| ApiClass() || ApiExtensionType() => MemberSortKey( | |
| isInstanceMember: isInstance, | |
| category: MemberCategory.interface, | |
| name: element.name, | |
| isSetter: false, | |
| ), | |
| ApiExtension() => MemberSortKey( | |
| isInstanceMember: isInstance, | |
| category: MemberCategory.extension, | |
| name: element.name, | |
| isSetter: false, | |
| ), | |
| ApiExecutable(:final kind, :final isStatic) => MemberSortKey( | |
| isInstanceMember: isInstance && !(isTopLevel || isStatic), | |
| category: switch (kind) { | |
| ApiExecutableKind.constructor => MemberCategory.constructor, | |
| ApiExecutableKind.getter || | |
| ApiExecutableKind.setter => MemberCategory.propertyAccessor, | |
| _ => MemberCategory.topLevelFunctionOrMethod, | |
| }, | |
| name: element.name, | |
| isSetter: kind == ApiExecutableKind.setter, | |
| ), | |
| ApiTypeAlias() => MemberSortKey( | |
| isInstanceMember: isInstance, | |
| category: MemberCategory.typeAlias, | |
| name: element.name, | |
| isSetter: false, | |
| ), | |
| }; | |
| MemberSortKey _computeSortKeyForDangling(ApiInterfaceType type) => | |
| MemberSortKey( | |
| isInstanceMember: false, | |
| category: MemberCategory.interface, | |
| name: type.name, | |
| isSetter: false, | |
| ); |