| // 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 'dart:io'; |
| |
| 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_declaration.dart'; |
| import 'api_summary_customizer.dart'; |
| import 'api_type.dart'; |
| import 'extensions.dart'; |
| |
| /// Traverses the public libraries of [packageName] within [context] to build |
| /// a canonical [ApiSummary] representation. |
| /// |
| /// Filtering and discovery behaviors are customized using [customizer]. |
| Future<ApiSummary> buildApiPackage( |
| String packageName, |
| AnalysisContext context, |
| ApiSummaryCustomizer customizer, |
| ) => _ApiBuilder(packageName, customizer).build(context); |
| |
| /// Internal builder traversing analysis results to build structured API |
| /// summaries. |
| final class _ApiBuilder { |
| final ApiSummaryCustomizer _customizer; |
| final String _pkgName; |
| |
| final _immediateSubinterfaceCache = |
| <LibraryElement, Map<ClassElement, Set<InterfaceElement>>>{}; |
| |
| final _processedElements = <Element>{}; |
| final _pendingElements = Queue<Element>(); |
| final _libraryBuilders = <String, _ApiLibraryBuilder>{}; |
| final _elementToLibraries = <Element, List<LibraryElement>>{}; |
| |
| _ApiBuilder(this._pkgName, this._customizer); |
| |
| Future<ApiSummary> build(AnalysisContext context) async { |
| final publicApiLibraries = <LibraryElement>[]; |
| final topLevelPublicElements = <Element>{}; |
| final scanContext = ApiSummaryContext( |
| packageName: _pkgName, |
| analysisContext: context, |
| publicApiLibraries: publicApiLibraries, |
| topLevelPublicElements: topLevelPublicElements, |
| ); |
| await _customizer.setupComplete(scanContext); |
| |
| await _scanPublicApiLibraries( |
| context, |
| topLevelPublicElements, |
| publicApiLibraries, |
| ); |
| await _customizer.initialScanComplete(scanContext); |
| |
| while (_pendingElements.isNotEmpty) { |
| final element = _pendingElements.removeFirst(); |
| if (!_processedElements.add(element)) continue; |
| |
| final isReferenced = !(element.library?.uri.isIn(_pkgName) ?? false); |
| |
| final ApiDeclarationStatus status; |
| if (topLevelPublicElements.contains(element)) { |
| status = ApiDeclarationStatus.public; |
| } else if (!isReferenced) { |
| status = ApiDeclarationStatus.nonPublic; |
| } else { |
| status = ApiDeclarationStatus.referenced; |
| } |
| |
| final showDetails = _customizer.shouldShowDetails(element, scanContext); |
| if (!showDetails && !_customizer.includeReferencedTypes) continue; |
| |
| final libraries = |
| _elementToLibraries[element] ?? |
| [if (element.library != null) element.library!]; |
| |
| final apiElement = switch (element) { |
| EnumElement() => _buildClass( |
| element, |
| includeMembers: showDetails, |
| status: status, |
| ), |
| MixinElement() => _buildClass( |
| element, |
| includeMembers: showDetails, |
| status: status, |
| ), |
| ClassElement() => _buildClass( |
| element, |
| includeMembers: showDetails, |
| status: status, |
| ), |
| ExtensionElement() => _buildExtension( |
| element, |
| includeMembers: showDetails, |
| status: status, |
| ), |
| ExtensionTypeElement() => _buildExtensionType( |
| element, |
| includeMembers: showDetails, |
| status: status, |
| ), |
| TopLevelFunctionElement() => _buildExecutable(element, status: status), |
| PropertyAccessorElement() => _buildExecutable(element, status: status), |
| TypeAliasElement() => _buildTypeAlias(element, status: status), |
| _ => null, |
| }; |
| |
| if (apiElement == null) { |
| stderr.writeln( |
| 'Warning: Skipping unsupported top-level element "${element.name}" ' |
| '(${element.runtimeType})', |
| ); |
| continue; |
| } |
| |
| for (final library in libraries) { |
| final builder = _libraryBuilders.putIfAbsent( |
| library.uri.toString(), |
| () => _ApiLibraryBuilder( |
| library.uri.toString(), |
| isPublicEntryPoint: library.uri.isInPublicLibOf(_pkgName), |
| ), |
| ); |
| switch (apiElement) { |
| case ApiClass(): |
| switch (element) { |
| case EnumElement(): |
| builder.enums.add(apiElement); |
| case MixinElement(): |
| builder.mixins.add(apiElement); |
| default: |
| builder.classes.add(apiElement); |
| } |
| case ApiExtension(): |
| builder.extensions.add(apiElement); |
| case ApiExtensionType(): |
| builder.extensionTypes.add(apiElement); |
| case ApiExecutable(): |
| builder.functions.add(apiElement); |
| case ApiTypeAlias(): |
| builder.typeAliases.add(apiElement); |
| } |
| } |
| } |
| |
| final libraries = _libraryBuilders.values.map((e) => e.build()).toList(); |
| |
| return ApiSummary(name: _pkgName, libraries: libraries); |
| } |
| |
| Future<void> _scanPublicApiLibraries( |
| AnalysisContext context, |
| Set<Element> topLevelPublicElements, |
| List<LibraryElement> publicApiLibraries, |
| ) async { |
| for (final file in context.contextRoot.analyzedFiles().sorted()) { |
| if (!file.endsWith('.dart')) continue; |
| final fileResult = context.currentSession.getFile(file); |
| if (fileResult is! FileResult) continue; |
| final uri = fileResult.uri; |
| if (fileResult.isLibrary && uri.isInPublicLibOf(_pkgName)) { |
| final resolvedLibraryResult = await context.currentSession |
| .getResolvedLibrary(file); |
| if (resolvedLibraryResult is! ResolvedLibraryResult) continue; |
| final library = resolvedLibraryResult.element; |
| final definedNames = library.exportNamespace.definedNames2; |
| for (final key in definedNames.keys.sorted()) { |
| final element = definedNames[key]!; |
| topLevelPublicElements.add(element); |
| (_elementToLibraries[element] ??= []).add(library); |
| _maybeAddPending(element); |
| } |
| publicApiLibraries.add(library); |
| _libraryBuilders.putIfAbsent( |
| library.uri.toString(), |
| () => _ApiLibraryBuilder( |
| library.uri.toString(), |
| isPublicEntryPoint: true, |
| ), |
| ); |
| } |
| } |
| } |
| |
| void _maybeAddPending(Element element) { |
| if (_processedElements.contains(element)) return; |
| _pendingElements.add(element); |
| } |
| |
| bool _isNullable(NullabilitySuffix suffix) => switch (suffix) { |
| NullabilitySuffix.none => false, |
| NullabilitySuffix.question => true, |
| NullabilitySuffix.star => throw UnsupportedError( |
| 'Legacy nullability (.star) is not supported.', |
| ), |
| }; |
| |
| bool _isVisibleForTesting(Element element) => |
| element.nonSynthetic.metadata.hasVisibleForTesting; |
| |
| ApiType _describeType(DartType type) { |
| final isNullable = _isNullable(type.nullabilitySuffix); |
| switch (type) { |
| case DynamicType(): |
| return const ApiDynamicType(); |
| case FunctionType( |
| :final returnType, |
| :final typeParameters, |
| :final formalParameters, |
| ): |
| final apiParameters = <ApiParameter>[]; |
| for (final p in formalParameters) { |
| apiParameters.add( |
| ApiParameter( |
| name: p.name ?? '', |
| type: _describeType(p.type), |
| isRequired: p.isRequired, |
| isNamed: p.isNamed, |
| isOptionalPositional: p.isOptionalPositional, |
| isDeprecated: p.isDeprecated, |
| ), |
| ); |
| } |
| |
| return ApiFunctionType( |
| returnType: _describeType(returnType), |
| typeParameters: _extractTypeParameters(typeParameters), |
| parameters: apiParameters, |
| isNullable: isNullable, |
| ); |
| case InterfaceType(:final element, :final typeArguments): |
| _maybeAddPending(element); |
| return ApiInterfaceType( |
| name: element.name ?? '', |
| libraryUri: element.library.uri.toString(), |
| typeArguments: typeArguments.map(_describeType).toList(), |
| isNullable: isNullable, |
| ); |
| case RecordType(:final positionalFields, :final namedFields): |
| return ApiRecordType( |
| positionalFields: positionalFields |
| .map((f) => _describeType(f.type)) |
| .toList(), |
| namedFields: namedFields |
| .sortedBy((f) => f.name) |
| .map( |
| (f) => ApiRecordNamedField( |
| name: f.name, |
| type: _describeType(f.type), |
| ), |
| ) |
| .toList(), |
| isNullable: isNullable, |
| ); |
| case TypeParameterType(:final element): |
| return ApiTypeParameterType( |
| name: element.name ?? '', |
| isNullable: isNullable, |
| ); |
| case NeverType(): |
| return ApiInterfaceType( |
| name: 'Never', |
| libraryUri: 'dart:core', |
| typeArguments: const [], |
| isNullable: isNullable, |
| ); |
| case VoidType(): |
| return const ApiVoidType(); |
| // When analyzing a workspace with mid-edit syntax errors or unresolved |
| // identifiers (e.g., unimported annotations), the analyzer produces an |
| // InvalidType. We gracefully fall back to dynamic to avoid crashing. |
| case InvalidType(): |
| return const ApiDynamicType(); |
| case dynamic(:final runtimeType): |
| stderr.writeln( |
| 'Warning: Encountered unexpected DartType "$runtimeType", ' |
| 'falling back to dynamic', |
| ); |
| return const ApiDynamicType(); |
| } |
| } |
| |
| Map<String, ApiType?> _extractTypeParameters( |
| List<TypeParameterElement> elements, |
| ) { |
| final result = <String, ApiType?>{}; |
| for (final e in elements) { |
| if (e.name != null) { |
| result[e.name!] = e.bound != null ? _describeType(e.bound!) : null; |
| } |
| } |
| return result; |
| } |
| |
| Map<ClassElement, Set<InterfaceElement>> |
| _getOrComputeImmediateSubinterfaceMap(LibraryElement library) { |
| if (_immediateSubinterfaceCache[library] case final m?) return m; |
| final result = <ClassElement, Set<InterfaceElement>>{}; |
| for (final interface in [ |
| ...library.classes, |
| ...library.mixins, |
| ...library.enums, |
| ...library.extensionTypes, |
| ]..sortBy((e) => e.name ?? '')) { |
| for (final superinterface in [ |
| interface.supertype, |
| ...interface.interfaces, |
| ...interface.mixins, |
| if (interface is MixinElement) ...interface.superclassConstraints, |
| ]) { |
| if (superinterface == null) continue; |
| final superinterfaceElement = superinterface.element; |
| if (superinterfaceElement is ClassElement && |
| superinterfaceElement.isSealed) { |
| (result[superinterfaceElement] ??= {}).add(interface); |
| } |
| } |
| } |
| _immediateSubinterfaceCache[library] = result; |
| return result; |
| } |
| |
| ApiClass _buildClass( |
| InterfaceElement element, { |
| required bool includeMembers, |
| required ApiDeclarationStatus status, |
| }) { |
| final constructors = <ApiExecutable>[]; |
| final methods = <ApiExecutable>[]; |
| |
| if (includeMembers) { |
| for (final member in element.children.sortedBy((m) => m.name ?? '')) { |
| if (member.name?.startsWith('_') ?? false) continue; |
| |
| switch (member) { |
| case ConstructorElement(): |
| if (element is ClassElement && |
| element.isAbstract && |
| (element.isFinal || element.isInterface || element.isSealed) && |
| !member.isFactory) { |
| continue; |
| } |
| if (element is EnumElement && !member.isFactory) continue; |
| constructors.add(_buildExecutable(member, status: status)); |
| case ExecutableElement(): |
| methods.add(_buildExecutable(member, status: status)); |
| } |
| } |
| } |
| |
| final modifiers = <ApiClassModifier>[]; |
| var immediateSubtypes = <String>[]; |
| |
| if (element is ClassElement) { |
| if (element.isSealed) { |
| modifiers.add(ApiClassModifier.isSealed); |
| final subinterfaces = _getOrComputeImmediateSubinterfaceMap( |
| element.library, |
| )[element]; |
| if (subinterfaces != null) { |
| immediateSubtypes = subinterfaces.map((e) => e.name ?? '').toList(); |
| immediateSubtypes.sort(); |
| } |
| } else { |
| if (element.isAbstract) modifiers.add(ApiClassModifier.isAbstract); |
| if (element.isBase) modifiers.add(ApiClassModifier.isBase); |
| if (element.isMixinClass) modifiers.add(ApiClassModifier.isMixin); |
| if (element.isInterface) modifiers.add(ApiClassModifier.isInterface); |
| if (element.isFinal) modifiers.add(ApiClassModifier.isFinal); |
| } |
| } else if (element is MixinElement) { |
| if (element.isBase) modifiers.add(ApiClassModifier.isBase); |
| } |
| return ApiClass( |
| name: element.name ?? '', |
| locationUri: element.library.uri.toString(), |
| status: status, |
| typeParameters: _extractTypeParameters(element.typeParameters), |
| supertype: element is ClassElement |
| ? (element.supertype != null |
| ? _describeType(element.supertype!) |
| : null) |
| : null, |
| interfaces: element.interfaces.map(_describeType).toList(), |
| mixins: element.mixins.map(_describeType).toList(), |
| superclassConstraints: element is MixinElement |
| ? element.superclassConstraints.map(_describeType).toList() |
| : const [], |
| modifiers: modifiers, |
| immediateSubtypes: immediateSubtypes, |
| constructors: constructors, |
| methods: methods, |
| isExperimental: element.nonSynthetic.metadata.hasExperimental, |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, |
| isVisibleForTesting: _isVisibleForTesting(element), |
| ); |
| } |
| |
| ApiExtension _buildExtension( |
| ExtensionElement element, { |
| required bool includeMembers, |
| required ApiDeclarationStatus status, |
| }) { |
| final methods = <ApiExecutable>[]; |
| if (includeMembers) { |
| for (final member in element.children.sortedBy((m) => m.name ?? '')) { |
| if (member.name?.startsWith('_') ?? false) continue; |
| if (member is FieldElement) continue; |
| if (member is ExecutableElement) { |
| methods.add(_buildExecutable(member, status: status)); |
| } |
| } |
| } |
| return ApiExtension( |
| name: element.name ?? '', |
| locationUri: element.library.uri.toString(), |
| status: status, |
| extendedType: _describeType(element.extendedType), |
| typeParameters: _extractTypeParameters(element.typeParameters), |
| methods: methods, |
| isExperimental: element.nonSynthetic.metadata.hasExperimental, |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, |
| isVisibleForTesting: _isVisibleForTesting(element), |
| ); |
| } |
| |
| ApiExtensionType _buildExtensionType( |
| ExtensionTypeElement element, { |
| required bool includeMembers, |
| required ApiDeclarationStatus status, |
| }) { |
| final constructors = <ApiExecutable>[]; |
| final methods = <ApiExecutable>[]; |
| if (includeMembers) { |
| for (final member in element.children.sortedBy((m) => m.name ?? '')) { |
| if (member.name?.startsWith('_') ?? false) continue; |
| if (member is FieldElement) continue; |
| if (member is ConstructorElement) { |
| constructors.add(_buildExecutable(member, status: status)); |
| } else if (member is ExecutableElement) { |
| methods.add(_buildExecutable(member, status: status)); |
| } |
| } |
| } |
| return ApiExtensionType( |
| name: element.name ?? '', |
| locationUri: element.library.uri.toString(), |
| status: status, |
| representationType: _describeType(element.representation.type), |
| typeParameters: _extractTypeParameters(element.typeParameters), |
| interfaces: element.interfaces.map(_describeType).toList(), |
| constructors: constructors, |
| methods: methods, |
| isExperimental: element.nonSynthetic.metadata.hasExperimental, |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, |
| isVisibleForTesting: _isVisibleForTesting(element), |
| ); |
| } |
| |
| ApiExecutable _buildExecutable( |
| ExecutableElement element, { |
| required ApiDeclarationStatus status, |
| }) { |
| final nonSyntheticElement = element.nonSynthetic; |
| final formalParameters = element.type.formalParameters; |
| |
| final kind = switch (element) { |
| GetterElement() => ApiExecutableKind.getter, |
| SetterElement() => ApiExecutableKind.setter, |
| MethodElement() => ApiExecutableKind.method, |
| ConstructorElement() => ApiExecutableKind.constructor, |
| _ => ApiExecutableKind.function, |
| }; |
| |
| final isConst = switch (element) { |
| ConstructorElement() => element.isConst, |
| PropertyAccessorElement() => element.variable.isConst, |
| _ => false, |
| }; |
| |
| final isEnumConstant = switch (element) { |
| PropertyAccessorElement(:final variable) => |
| variable is FieldElement && variable.isEnumConstant, |
| _ => false, |
| }; |
| return ApiExecutable( |
| name: element.apiName, |
| locationUri: element.library.uri.toString(), |
| status: status, |
| kind: kind, |
| returnType: _describeType(element.returnType), |
| typeParameters: _extractTypeParameters(element.typeParameters), |
| parameters: formalParameters |
| .map( |
| (e) => ApiParameter( |
| name: e.name ?? '', |
| type: _describeType(e.type), |
| isRequired: e.isRequired, |
| isNamed: e.isNamed, |
| isOptionalPositional: e.isOptionalPositional, |
| isDeprecated: e.isDeprecated, |
| ), |
| ) |
| .toList(), |
| isStatic: element.isStatic, |
| isConst: isConst, |
| isEnumConstant: isEnumConstant, |
| isDeprecated: nonSyntheticElement.metadata.hasDeprecated, |
| isExperimental: nonSyntheticElement.metadata.hasExperimental, |
| isVisibleForTesting: _isVisibleForTesting(element), |
| ); |
| } |
| |
| ApiTypeAlias _buildTypeAlias( |
| TypeAliasElement element, { |
| required ApiDeclarationStatus status, |
| }) => ApiTypeAlias( |
| name: element.name ?? '', |
| locationUri: element.library.uri.toString(), |
| status: status, |
| typeParameters: _extractTypeParameters(element.typeParameters), |
| aliasedType: _describeType(element.aliasedType), |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, |
| isExperimental: element.nonSynthetic.metadata.hasExperimental, |
| isVisibleForTesting: _isVisibleForTesting(element), |
| ); |
| } |
| |
| /// Mutable builder accumulating declarations for a single library. |
| final class _ApiLibraryBuilder { |
| final String uri; |
| final bool isPublicEntryPoint; |
| final classes = <ApiClass>[]; |
| final enums = <ApiClass>[]; |
| final mixins = <ApiClass>[]; |
| final extensions = <ApiExtension>[]; |
| final extensionTypes = <ApiExtensionType>[]; |
| final functions = <ApiExecutable>[]; |
| final typeAliases = <ApiTypeAlias>[]; |
| |
| _ApiLibraryBuilder(this.uri, {required this.isPublicEntryPoint}); |
| |
| ApiLibrary build() { |
| classes.sortBy((e) => e.name); |
| enums.sortBy((e) => e.name); |
| mixins.sortBy((e) => e.name); |
| extensions.sortBy((e) => e.name); |
| extensionTypes.sortBy((e) => e.name); |
| functions.sortBy((e) => e.name); |
| typeAliases.sortBy((e) => e.name); |
| return ApiLibrary( |
| uri: uri, |
| isPublicEntryPoint: isPublicEntryPoint, |
| classes: classes, |
| enums: enums, |
| mixins: mixins, |
| extensions: extensions, |
| extensionTypes: extensionTypes, |
| functions: functions, |
| typeAliases: typeAliases, |
| ); |
| } |
| } |