| // 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_facet.dart'; | |
| import 'api_summary_customizer.dart'; | |
| import 'api_type.dart'; | |
| import 'extensions.dart'; | |
| import 'js_facet.dart'; | |
| import 'meta_facet.dart'; | |
| /// Traverses the public libraries of [packageName] within [context] to build | |
| /// a canonical [ApiSummary] representation. | |
| /// | |
| /// Filtering and discovery behaviors are customized using [customizer]. | |
| /// | |
| /// [environment] defines the SDK and platform environment constraints of the | |
| /// package (such as 'sdk' or 'flutter'). | |
| /// | |
| /// [executables] defines the executables exposed by the package, mapping | |
| /// executable names to their target script paths within `bin/`. | |
| Future<ApiSummary> buildApiPackage( | |
| String packageName, | |
| AnalysisContext context, | |
| ApiSummaryCustomizer customizer, { | |
| Map<String, String> environment = const {}, | |
| Map<String, String?> executables = const {}, | |
| }) => _ApiBuilder( | |
| packageName, | |
| customizer, | |
| environment: environment, | |
| executables: executables, | |
| ).build(context); | |
| /// Internal builder traversing analysis results to build structured API | |
| /// summaries. | |
| final class _ApiBuilder { | |
| final ApiSummaryCustomizer _customizer; | |
| final String _pkgName; | |
| final Map<String, String> _environment; | |
| final Map<String, String?> _executables; | |
| 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, { | |
| this._environment = const {}, | |
| this._executables = const {}, | |
| }); | |
| 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), | |
| facets: _extractFacets(library), | |
| ), | |
| ); | |
| 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, | |
| environment: _environment, | |
| executables: _executables, | |
| 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, | |
| facets: _extractFacets(library), | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| 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.', | |
| ), | |
| }; | |
| 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, | |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, | |
| facets: _extractFacets(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, | |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, | |
| facets: _extractFacets(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, | |
| isDeprecated: element.nonSynthetic.metadata.hasDeprecated, | |
| facets: _extractFacets(element), | |
| ); | |
| } | |
| bool _isDeprecated(Element element) => | |
| element.nonSynthetic.metadata.hasDeprecated || | |
| (element is PropertyAccessorElement && | |
| element.variable.nonSynthetic.metadata.hasDeprecated); | |
| List<ApiFacet> _extractFacets(Element element) => [ | |
| ?MetaContractFacet.fromElement(element), | |
| ?JsBindingFacet.fromElement(element), | |
| ?JsExportFacet.fromElement(element), | |
| ]; | |
| ApiExecutable _buildExecutable( | |
| ExecutableElement element, { | |
| required ApiDeclarationStatus status, | |
| }) { | |
| 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, | |
| isDeprecated: _isDeprecated(element), | |
| isEnumConstant: isEnumConstant, | |
| facets: _extractFacets(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, | |
| facets: _extractFacets(element), | |
| ); | |
| } | |
| /// Mutable builder accumulating declarations for a single library. | |
| final class _ApiLibraryBuilder { | |
| final String uri; | |
| final bool isPublicEntryPoint; | |
| final List<ApiFacet> facets; | |
| 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, | |
| this.facets = const [], | |
| }); | |
| 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, | |
| facets: facets, | |
| ); | |
| } | |
| } |