analyze method
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,
);
}