Skip to content

Commit

Permalink
Add minimal AnalysisDriverModel implementation (#3814)
Browse files Browse the repository at this point in the history
* Make `resolver_test.dart` test `AnalysisDriverModel` exactly like it
tests `BuildAssetUriResolver`.

Add minimal implementation to `AnalysisDriverModel` to make the tests pass.

Add TODOs where there is functionality not covered by tests that is
therefore not added.

* Address review comments.

* Address review comments.
  • Loading branch information
davidmorgan authored Feb 4, 2025
1 parent 1fe2657 commit 5c1ddd4
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 229 deletions.
3 changes: 2 additions & 1 deletion build_resolvers/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## 2.4.4-wip

- Refactor `BuildAssetUriResolver` into `AnalysisDriverModel` and
`AnalysisDriverModelUriResolver`.
`AnalysisDriverModelUriResolver`. Add new implementation of
`AnalysisDriverModel`.

## 2.4.3

Expand Down
129 changes: 122 additions & 7 deletions build_resolvers/lib/src/analysis_driver_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:collection';

import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
// ignore: implementation_imports
import 'package:analyzer/src/clients/build_resolvers/build_resolvers.dart';
import 'package:build/build.dart';
import 'package:path/path.dart' as p;

import 'analysis_driver_model_uri_resolver.dart';

/// Manages analysis driver and related build state.
///
Expand All @@ -19,36 +25,145 @@ import 'package:build/build.dart';
/// - Maintains an in-memory filesystem that is the analyzer's view of the
/// build.
/// - Notifies the analyzer of changes to that in-memory filesystem.
abstract class AnalysisDriverModel {
///
/// TODO(davidmorgan): the implementation here is unfinished and not used
/// anywhere; finish it. See `build_asset_uri_resolver.dart` for the current
/// implementation.
class AnalysisDriverModel {
/// In-memory filesystem for the analyzer.
abstract final MemoryResourceProvider resourceProvider;
final MemoryResourceProvider resourceProvider =
MemoryResourceProvider(context: p.posix);

/// Notifies that [step] has completed.
///
/// All build steps must complete before [reset] is called.
void notifyComplete(BuildStep step);
void notifyComplete(BuildStep step) {
// TODO(davidmorgan): add test coverage, fix implementation.
}

/// Clear cached information specific to an individual build.
void reset();
void reset() {
// TODO(davidmorgan): add test coverage, fix implementation.
}

/// Attempts to parse [uri] into an [AssetId] and returns it if it is cached.
///
/// Handles 'package:' or 'asset:' URIs, as well as 'file:' URIs of the form
/// `/$packageName/$assetPath`.
///
/// Returns null if the Uri cannot be parsed or is not cached.
AssetId? lookupCachedAsset(Uri uri);
/// Returns null if the `Uri` cannot be parsed or is not cached.
AssetId? lookupCachedAsset(Uri uri) {
final assetId = AnalysisDriverModelUriResolver.parseAsset(uri);
// TODO(davidmorgan): not clear if this is the right "exists" check.
if (assetId == null || !resourceProvider.getFile(assetId.asPath).exists) {
return null;
}

return assetId;
}

/// Updates [resourceProvider] and the analysis driver given by
/// `withDriverResource` with updated versions of [entryPoints].
///
/// If [transitive], then all the transitive imports from [entryPoints] are
/// also updated.
///
/// Notifies [buildStep] of all inputs that result from analysis. If
/// [transitive], this includes all transitive dependencies.
///
/// If while finding transitive deps a `.transitive_deps` file is
/// encountered next to a source file then this cuts off the reporting
/// of deps to the [buildStep], but does not affect the reporting of
/// files to the analysis driver.
Future<void> performResolve(
BuildStep buildStep,
List<AssetId> entryPoints,
Future<void> Function(
FutureOr<void> Function(AnalysisDriverForPackageBuild))
withDriverResource,
{required bool transitive});
{required bool transitive}) async {
/// TODO(davidmorgan): add test coverage for whether transitive
/// sources are read when [transitive] is false, fix the implementation
/// here.
/// TODO(davidmorgan): add test coverage for whether
/// `.transitive_deps` files cut off the reporting of deps to the
/// [buildStep], fix the implementation here.
// Find transitive deps, this also informs [buildStep] of all inputs).
final ids = await _expandToTransitive(buildStep, entryPoints);

// Apply changes to in-memory filesystem.
for (final id in ids) {
if (await buildStep.canRead(id)) {
final content = await buildStep.readAsString(id);

/// TODO(davidmorgan): add test coverage for when a file is
/// modified rather than added, fix the implementation here.
resourceProvider.newFile(id.asPath, content);
} else {
if (resourceProvider.getFile(id.asPath).exists) {
resourceProvider.deleteFile(id.asPath);
}
}
}

// Notify the analyzer of changes.
await withDriverResource((driver) async {
for (final id in ids) {
// TODO(davidmorgan): add test coverage for over-notification of
// changes, fix the implementaion here.
driver.changeFile(id.asPath);
}
await driver.applyPendingFileChanges();
});
}

/// Walks the import graph from [ids], returns full transitive deps.
Future<Set<AssetId>> _expandToTransitive(
AssetReader reader, Iterable<AssetId> ids) async {
final result = ids.toSet();
final nextIds = Queue.of(ids);
while (nextIds.isNotEmpty) {
final nextId = nextIds.removeFirst();

// Skip if not readable. Note that calling `canRead` still makes it a
// dependency of the `BuildStep`.
if (!await reader.canRead(nextId)) continue;

final content = await reader.readAsString(nextId);
final deps = _parseDependencies(content, nextId);

// For each dep, if it's not in `result` yet, it's newly-discovered:
// add it to `nextIds`.
for (final dep in deps) {
if (result.add(dep)) {
nextIds.add(dep);
}
}
}
return result;
}
}

const _ignoredSchemes = ['dart', 'dart-ext'];

/// Parses Dart source in [content], returns all depedencies: all assets
/// mentioned in directives, excluding `dart:` and `dart-ext` schemes.
List<AssetId> _parseDependencies(String content, AssetId from) =>
parseString(content: content, throwIfDiagnostics: false)
.unit
.directives
.whereType<UriBasedDirective>()
.map((directive) => directive.uri.stringValue)
// Uri.stringValue can be null for strings that use interpolation.
.nonNulls
.where(
(uriContent) => !_ignoredSchemes.any(Uri.parse(uriContent).isScheme),
)
.map((content) => AssetId.resolve(Uri.parse(content), from: from))
.toList();

extension _AssetIdExtensions on AssetId {
/// Asset path for the in-memory filesystem.
String get asPath => AnalysisDriverModelUriResolver.assetPath(this);
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class AnalysisDriverModelUriResolver implements UriResolver {
return null;
}

/// Path of [assetId] for the in-memory filesystem.
static String assetPath(AssetId assetId) =>
p.posix.join('/${assetId.package}', assetId.path);
}
19 changes: 9 additions & 10 deletions build_resolvers/lib/src/resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,16 +412,15 @@ class AnalyzerResolvers implements Resolvers {
AnalysisOptions? analysisOptions,
Future<String> Function()? sdkSummaryGenerator,
PackageConfig? packageConfig,
}) {
final buildAssetUriResolver = BuildAssetUriResolver();
return AnalyzerResolvers._(
analysisOptions: analysisOptions,
sdkSummaryGenerator: sdkSummaryGenerator,
packageConfig: packageConfig,
// Custom resolvers get their own asset uri resolver, as there should
// always be a 1:1 relationship between them.
analysisDriverModel: buildAssetUriResolver);
}
AnalysisDriverModel? analysisDriverModel,
}) =>
AnalyzerResolvers._(
analysisOptions: analysisOptions,
sdkSummaryGenerator: sdkSummaryGenerator,
packageConfig: packageConfig,
// Custom resolvers get their own asset uri resolver by default as
// there should always be a 1:1 relationship between them.
analysisDriverModel: analysisDriverModel ?? BuildAssetUriResolver());

/// See [AnalyzerResolvers.custom] for docs.
@Deprecated('Use either the AnalyzerResolvers.custom constructor or the '
Expand Down
Loading

0 comments on commit 5c1ddd4

Please sign in to comment.