analyze method

Future<PackageApi> analyze()

analyzes the configured package and returns a model of its public API

Implementation

Future<PackageApi> analyze() async {
  final normalizedAbsoluteProjectPath =
      _getNormalizedAbsolutePath(packagePath);

  final yamlContent =
      await File(path.join(normalizedAbsoluteProjectPath, 'pubspec.yaml'))
          .readAsString();
  final pubSpec = Pubspec.parse(yamlContent);

  final resourceProvider = PhysicalResourceProvider.INSTANCE;

  final normalizedAbsolutePublicEntrypointPath = _getNormalizedAbsolutePath(
      path.join(normalizedAbsoluteProjectPath, 'lib'));

  final contextCollection = _createAnalysisContextCollection(
    path: normalizedAbsoluteProjectPath,
    resourceProvider: resourceProvider,
  );

  final collectedInterfaces = <int?, _InterfaceCollectionResult>{};

  final analyzedFiles = List<_FileToAnalyzeEntry>.empty(growable: true);
  final filesToAnalyze = Queue<_FileToAnalyzeEntry>();
  filesToAnalyze.addAll(
      _findPublicFilesInProject(normalizedAbsolutePublicEntrypointPath));

  final typeHierarchy = TypeHierarchy.empty();

  while (filesToAnalyze.isNotEmpty) {
    final fileToAnalyze = filesToAnalyze.first;
    filesToAnalyze.removeFirst();
    analyzedFiles.add(fileToAnalyze);

    final relativeFilePath = path.relative(fileToAnalyze.filePath,
        from: normalizedAbsolutePublicEntrypointPath);

    try {
      final context = contextCollection.contextFor(fileToAnalyze.filePath);

      final unitResult = await context.currentSession
          .getResolvedUnit(fileToAnalyze.filePath);
      if (unitResult is ResolvedUnitResult) {
        if (!unitResult.isPart) {
          final collector = APIRelevantElementsCollector(
            shownNames: fileToAnalyze.shownNames,
            hiddenNames: fileToAnalyze.hiddenNames,
            rootPath: normalizedAbsoluteProjectPath,
            typeHierarchy: typeHierarchy,
          );
          unitResult.libraryElement.accept(collector);
          final skippedInterfaces = <int>[];
          for (final cd in collector.interfaceDeclarations) {
            if (!collectedInterfaces.containsKey(cd.id)) {
              collectedInterfaces[cd.id] = _InterfaceCollectionResult();
            }
            if (!collectedInterfaces[cd.id]!
                .interfaceDeclarations
                .any((cdToCheck) => cdToCheck.id == cd.id)) {
              collectedInterfaces[cd.id]!.interfaceDeclarations.add(cd);
            } else {
              skippedInterfaces.add(cd.id);
            }

            // only register the entry points if the element is collected directly (and not transitively)
            if (collector.directlyCollectedElementIds.contains(cd.id)) {
              // set entry point
              _addEntryPoints<InternalInterfaceDeclaration>(
                collectedInterfaces[cd.id]!.interfaceDeclarations,
                cd.id,
                {
                  if (_isPublicEntryPoint(relativeFilePath)) relativeFilePath,
                  ...fileToAnalyze.exportedBy
                },
              );
            }
          }
          for (final exd in collector.executableDeclarations) {
            if (skippedInterfaces.contains(exd.parentClassId)) {
              continue;
            }
            if (!collectedInterfaces.containsKey(exd.parentClassId)) {
              collectedInterfaces[exd.parentClassId] =
                  _InterfaceCollectionResult();
            }
            final exdAlreadyExists = collectedInterfaces[exd.parentClassId]!
                .executableDeclarations
                .any((element) => element.id == exd.id);
            if (!exdAlreadyExists) {
              collectedInterfaces[exd.parentClassId]!
                  .executableDeclarations
                  .add(exd);
            }
            if (exd.parentClassId == null &&
                collector.directlyCollectedElementIds.contains(exd.id)) {
              // we only store the entry point on root elements
              // only register the entry points if the element is collected directly (and not transitively)
              _addEntryPoints<InternalExecutableDeclaration>(
                collectedInterfaces[exd.parentClassId]!
                    .executableDeclarations,
                exd.id,
                {
                  if (_isPublicEntryPoint(relativeFilePath)) relativeFilePath,
                  ...fileToAnalyze.exportedBy
                },
              );
            }
          }
          for (final fd in collector.fieldDeclarations) {
            if (skippedInterfaces.contains(fd.parentClassId)) {
              continue;
            }
            if (!collectedInterfaces.containsKey(fd.parentClassId)) {
              collectedInterfaces[fd.parentClassId] =
                  _InterfaceCollectionResult();
            }
            final fdAlreadyExists = collectedInterfaces[fd.parentClassId]!
                .fieldDeclarations
                .any((element) => element.id == fd.id);
            if (!fdAlreadyExists) {
              collectedInterfaces[fd.parentClassId]!
                  .fieldDeclarations
                  .add(fd);
            }
            if (fd.parentClassId == null &&
                collector.directlyCollectedElementIds.contains(fd.id)) {
              // we only store the entry point on root elements
              // only register the entry points if the element is collected directly (and not transitively)
              _addEntryPoints<InternalFieldDeclaration>(
                collectedInterfaces[fd.parentClassId]!.fieldDeclarations,
                fd.id,
                {
                  if (_isPublicEntryPoint(relativeFilePath)) relativeFilePath,
                  ...fileToAnalyze.exportedBy
                },
              );
            }
          }
          for (final tad in collector.typeAliasDeclarations) {
            if (skippedInterfaces.contains(tad.parentClassId)) {
              continue;
            }
            if (!collectedInterfaces.containsKey(tad.parentClassId)) {
              collectedInterfaces[tad.parentClassId] =
                  _InterfaceCollectionResult();
            }
            final tadAlreadyExists = collectedInterfaces[tad.parentClassId]!
                .typeAliasDeclarations
                .any((element) => element.id == tad.id);
            if (!tadAlreadyExists) {
              collectedInterfaces[tad.parentClassId]!
                  .typeAliasDeclarations
                  .add(tad);
            }
            if (tad.parentClassId == null &&
                collector.directlyCollectedElementIds.contains(tad.id)) {
              // we only store the entry point on root elements
              // only register the entry points if the element is collected directly (and not transitively)
              _addEntryPoints<InternalTypeAliasDeclaration>(
                collectedInterfaces[tad.parentClassId]!.typeAliasDeclarations,
                tad.id,
                {
                  if (_isPublicEntryPoint(relativeFilePath)) relativeFilePath,
                  ...fileToAnalyze.exportedBy
                },
              );
            }
          }
          for (final tu in collector.typeUsages.keys) {
            collectedInterfaces[tu]
                ?.typeUsages
                .addAll(collector.typeUsages[tu]!);
          }
        }

        final referencedFilesCollector = ExportedFilesCollector();
        unitResult.libraryElement.accept(referencedFilesCollector);
        for (final fileRef in referencedFilesCollector.fileReferences) {
          if (!_isInternalRef(
              originLibrary: fileRef.originLibrary,
              refLibrary: fileRef.referencedLibrary)) {
            continue;
          }
          final relativeUri =
              _getRelativeUriFromLibraryIdentifier(fileRef.uri);
          final referencedFilePath = path.normalize(
              path.join(path.dirname(fileToAnalyze.filePath), relativeUri));
          final analyzeEntry = _FileToAnalyzeEntry(
            filePath: referencedFilePath,
            shownNames: fileRef.shownNames,
            hiddenNames: fileRef.hiddenNames,
            exportedBy: {
              ...fileToAnalyze.exportedBy,
              if (_isPublicEntryPoint(relativeFilePath)) relativeFilePath,
            },
          );
          if (!analyzedFiles.contains(analyzeEntry) &&
              !filesToAnalyze.contains(analyzeEntry)) {
            filesToAnalyze.add(analyzeEntry);
          }
        }
      }
    } on StateError catch (e) {
      logError('Problem parsing $fileToAnalyze: $e');
    }
  }

  final packageInterfaceDeclarations =
      List<InterfaceDeclaration>.empty(growable: true);
  final packageExecutableDeclarations =
      List<ExecutableDeclaration>.empty(growable: true);
  final packageFieldDeclarations =
      List<FieldDeclaration>.empty(growable: true);
  final packageTypeAliasDeclarations =
      List<TypeAliasDeclaration>.empty(growable: true);

  if (!collectedInterfaces.containsKey(null)) {
    collectedInterfaces[null] = _InterfaceCollectionResult();
  }

  // aggregate interface declarations
  for (final interfaceId in collectedInterfaces.keys) {
    final entry = collectedInterfaces[interfaceId]!;
    // for all non-root elements add the fields and executables to its class
    if (entry.interfaceDeclarations.isNotEmpty) {
      assert(entry.interfaceDeclarations.length == 1,
          'We found multiple classes sharing the same classId!');
      final cd = entry.interfaceDeclarations.single;
      cd.executableDeclarations.addAll(entry.executableDeclarations
          .map((e) => e.toExecutableDeclaration()));
      cd.fieldDeclarations
          .addAll(entry.fieldDeclarations.map((e) => e.toFieldDeclaration()));
    } else if (interfaceId != null) {
      // here we collected an element in the context of a class but the class is not available
      // in this case we print a warning and ignore them
      final executableList =
          entry.executableDeclarations.map((e) => e.name).join(', ');
      final fieldList = entry.fieldDeclarations.map((e) => e.name).join(', ');
      final typeAliasList =
          entry.typeAliasDeclarations.map((e) => e.name).join(', ');
      logWarning(
          'We encountered elements that are marked to belong to an interface but the interface is not collected!\nExecutables: $executableList\nFields: $fieldList\nTypeAliases: $typeAliasList');
    }
  }

  // remove collected elements that don't have their class collected (we merged the elements with root already)
  collectedInterfaces.removeWhere(
      (key, value) => key != null && value.interfaceDeclarations.isEmpty);

  _mergeSuperTypes(collectedInterfaces);

  // extract package declarations
  for (final classId in collectedInterfaces.keys) {
    final entry = collectedInterfaces[classId]!;
    if (entry.interfaceDeclarations.isEmpty) {
      packageExecutableDeclarations.addAll(entry.executableDeclarations
          .map((e) => e.toExecutableDeclaration()));
      packageFieldDeclarations
          .addAll(entry.fieldDeclarations.map((e) => e.toFieldDeclaration()));
      packageTypeAliasDeclarations.addAll(
          entry.typeAliasDeclarations.map((e) => e.toTypeAliasDeclaration()));
    } else {
      assert(entry.interfaceDeclarations.length == 1,
          'We found multiple classes sharing the same classId!');
      final cd = entry.interfaceDeclarations.single;
      packageInterfaceDeclarations
          .add(cd.toInterfaceDeclaration(typeUsages: entry.typeUsages));
    }
  }

  final normalizedProjectPath = path.normalize(path.absolute(packagePath));
  final androidPlatformConstraints = doAnalyzePlatformConstraints
      ? await AndroidPlatformConstraintsHelper.getAndroidPlatformConstraints(
          packagePath: normalizedProjectPath,
        )
      : null;
  final iosPlatformConstraints = doAnalyzePlatformConstraints
      ? await IOSPlatformConstraintsHelper.getIOSPlatformConstraints(
          packagePath: normalizedProjectPath,
        )
      : null;

  final sdkVersion = pubSpec.environment?['sdk'];
  Version? minSdkVersion;
  if (sdkVersion is VersionRange) {
    minSdkVersion = sdkVersion.min;
  } else if (sdkVersion is Version) {
    minSdkVersion = sdkVersion;
  }
  final isFlutter = pubSpec.dependencies.containsKey('flutter');
  final packageDependencies =
      PackageDependenciesHelper.getPackageDependencies(pubSpec);

  return PackageApi(
    packageName: pubSpec.name,
    packageVersion: pubSpec.version?.toString(),
    packagePath: normalizedProjectPath,
    interfaceDeclarations: packageInterfaceDeclarations,
    executableDeclarations: packageExecutableDeclarations,
    fieldDeclarations: packageFieldDeclarations,
    typeAliasDeclarations: packageTypeAliasDeclarations,
    semantics: semantics,
    androidPlatformConstraints: androidPlatformConstraints,
    iosPlatformConstraints: iosPlatformConstraints,
    sdkType: isFlutter ? SdkType.flutter : SdkType.dart,
    minSdkVersion: minSdkVersion ?? Version.none,
    packageDependencies: packageDependencies,
    typeHierarchy: typeHierarchy,
  );
}