Fine. Add URI resolution to library manifest signature.
Change-Id: I22de286eda1c3c89224baa99814800225b890330
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/450746
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analyzer/lib/src/dart/analysis/file_state.dart b/pkg/analyzer/lib/src/dart/analysis/file_state.dart
index 6a9d6c6..e6101c8 100644
--- a/pkg/analyzer/lib/src/dart/analysis/file_state.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/file_state.dart
@@ -1880,9 +1880,10 @@
LibraryCycle get libraryCycle {
if (_libraryCycle == null) {
computeLibraryCycle(
- file._fsState.withFineDependencies,
- file._fsState._saltForElements,
- this,
+ withFineDependencies: file._fsState.withFineDependencies,
+ saltForElements: file._fsState._saltForElements,
+ sourceFactory: file._fsState._sourceFactory,
+ file: this,
);
}
diff --git a/pkg/analyzer/lib/src/dart/analysis/library_graph.dart b/pkg/analyzer/lib/src/dart/analysis/library_graph.dart
index 77c6ce0..95adb35 100644
--- a/pkg/analyzer/lib/src/dart/analysis/library_graph.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/library_graph.dart
@@ -8,18 +8,25 @@
as graph
show DependencyWalker, Node;
import 'package:analyzer/src/dart/analysis/file_state.dart';
+import 'package:analyzer/src/dart/sdk/sdk.dart';
+import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/summary/api_signature.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:collection/collection.dart';
/// Ensure that the `FileState._libraryCycle` for the [file] and anything it
/// depends on is computed.
-void computeLibraryCycle(
- bool withFineDependencies,
- Uint32List salt,
- LibraryFileKind file,
-) {
- var libraryWalker = _LibraryWalker(withFineDependencies, salt);
+void computeLibraryCycle({
+ required bool withFineDependencies,
+ required Uint32List saltForElements,
+ required SourceFactory sourceFactory,
+ required LibraryFileKind file,
+}) {
+ var libraryWalker = _LibraryWalker(
+ withFineDependencies: withFineDependencies,
+ saltForElements: saltForElements,
+ sourceFactory: sourceFactory,
+ );
libraryWalker.walk(libraryWalker.getNode(file));
}
@@ -36,6 +43,9 @@
/// The URIs of [libraries].
final Set<Uri> libraryUris;
+ /// The transitive set of package names that this cycle references.
+ final Set<String> transitivePackages;
+
/// The library cycles that this cycle references directly.
final Set<LibraryCycle> directDependencies;
@@ -72,6 +82,7 @@
required this.withFineDependencies,
required this.libraries,
required this.libraryUris,
+ required this.transitivePackages,
required this.directDependencies,
required this.apiSignature,
required this.manifestSignature,
@@ -153,10 +164,15 @@
/// sorted [LibraryCycle]s.
class _LibraryWalker extends graph.DependencyWalker<_LibraryNode> {
final bool withFineDependencies;
- final Uint32List _salt;
+ final Uint32List saltForElements;
+ final SourceFactory sourceFactory;
final Map<LibraryFileKind, _LibraryNode> nodesOfFiles = {};
- _LibraryWalker(this.withFineDependencies, this._salt);
+ _LibraryWalker({
+ required this.withFineDependencies,
+ required this.saltForElements,
+ required this.sourceFactory,
+ });
@override
void evaluate(_LibraryNode v) {
@@ -166,7 +182,7 @@
@override
void evaluateScc(List<_LibraryNode> scc) {
var apiSignature = ApiSignature();
- apiSignature.addUint32List(_salt);
+ apiSignature.addUint32List(saltForElements);
// Sort libraries to produce stable signatures.
scc.sort((first, second) {
@@ -205,12 +221,23 @@
}
}
+ var transitivePackages = {
+ ...directDependencies.expand(
+ (dependency) => dependency.transitivePackages,
+ ),
+ ...libraries
+ .map((library) => library.file.uriProperties.packageName)
+ .nonNulls,
+ };
+
String manifestSignature;
String nonTransitiveApiSignature;
{
var manifestBuilder = ApiSignature();
var apiSignatureBuilder = ApiSignature();
- manifestBuilder.addUint32List(_salt);
+ manifestBuilder.addBytes(saltForElements);
+ _addUriResolutionToSignature(manifestBuilder, transitivePackages);
+
var sortedFiles = libraries
.expand((library) => library.files)
.sortedBy((file) => file.path);
@@ -228,6 +255,7 @@
withFineDependencies: withFineDependencies,
libraries: libraries.toFixedList(),
libraryUris: libraryUris,
+ transitivePackages: transitivePackages,
directDependencies: directDependencies,
apiSignature: apiSignature.toHex(),
manifestSignature: manifestSignature,
@@ -244,6 +272,55 @@
return nodesOfFiles.putIfAbsent(file, () => _LibraryNode(this, file));
}
+ /// Add URI resolution environment to [signature].
+ ///
+ /// Library manifests are reused across analysis contexts. If two contexts
+ /// resolve the same `package:` URI to different file system locations, the
+ /// manifests **must not** be considered interchangeable - otherwise we can
+ /// end up reusing a manifest built for a different package layout and get
+ /// mismatched element IDs.
+ ///
+ /// To make the manifest key sensitive to the resolution environment (without
+ /// over-invalidating), we:
+ ///
+ /// * include the SDK root path (when using a folder-based SDK), and
+ /// * include the file system paths of the **transitively referenced**
+ /// packages only - i.e. the packages in [packageNames], not every package
+ /// visible in the analysis context.
+ ///
+ /// This design strikes a balance:
+ /// * Different package configurations that map a dependency to different
+ /// locations produce different keys, forcing a rebuild where reuse would
+ /// be detrimental to element ID stability.
+ /// * Contexts whose *overall* resolution differs but that map the relevant
+ /// packages identically can still reuse manifests.
+ ///
+ /// Trade-off: a dependency package (or the SDK) moving on disk changes the
+ /// key for all cycles that depend on it, even if the package's *API* hasn't
+ /// changed. This is intentional - package dependencies change rarely, but
+ /// different package of the same (not well configured) workspace would
+ /// constantly churn new element IDs for unfortunate shared library manifest.
+ void _addUriResolutionToSignature(
+ ApiSignature signature,
+ Set<String> packageNames,
+ ) {
+ var sdk = sourceFactory.dartSdk;
+ if (sdk is FolderBasedDartSdk) {
+ signature.addString(sdk.directory.path);
+ }
+
+ var packageMap = sourceFactory.packageMap;
+ if (packageMap != null) {
+ var packagePaths = packageNames
+ .map((packageName) => packageMap[packageName])
+ .nonNulls
+ .expand((folders) => folders)
+ .map((folder) => folder.path)
+ .sorted();
+ signature.addStringList(packagePaths);
+ }
+ }
+
void _appendDirectlyReferenced(
Set<LibraryCycle> directDependencies,
ApiSignature apiSignature,
diff --git a/pkg/analyzer/test/src/dart/analysis/driver_test.dart b/pkg/analyzer/test/src/dart/analysis/driver_test.dart
index b531989..6f8e8db 100644
--- a/pkg/analyzer/test/src/dart/analysis/driver_test.dart
+++ b/pkg/analyzer/test/src/dart/analysis/driver_test.dart
@@ -79530,6 +79530,245 @@
);
}
+ test_manifest_key_differentPackages() async {
+ newFile('/packages/foo_v1/lib/foo.dart', r'''
+class A {}
+''');
+
+ newFile('/packages/foo_v2/lib/foo.dart', r'''
+class A extends A2 {}
+class A2 {}
+''');
+
+ newFile('/packages/bar/lib/bar.dart', r'''
+import 'package:foo/foo.dart';
+class B extends A {}
+''');
+
+ writePackageConfig(
+ '$workspaceRootPath/test_1',
+ PackageConfigFileBuilder()
+ ..add(name: 'foo', rootPath: '/packages/foo_v1')
+ ..add(name: 'bar', rootPath: '/packages/bar')
+ ..add(name: 'test', rootPath: '$workspaceRootPath/test_1'),
+ );
+
+ writePackageConfig(
+ '$workspaceRootPath/test_2',
+ PackageConfigFileBuilder()
+ ..add(name: 'foo', rootPath: '/packages/foo_v2')
+ ..add(name: 'bar', rootPath: '/packages/bar')
+ ..add(name: 'test', rootPath: '$workspaceRootPath/test_2'),
+ );
+
+ var testFile1 = newFile('$workspaceRootPath/test_1/lib/test.dart', r'''
+import 'package:bar/bar.dart';
+class X extends B {}
+''');
+
+ var testFile2 = newFile('$workspaceRootPath/test_2/lib/test.dart', r'''
+import 'package:bar/bar.dart';
+class X extends B {}
+''');
+
+ configuration
+ ..withGetLibraryByUri = false
+ ..withLibraryManifest = true
+ ..withLinkLibraryCycle = true
+ ..withSchedulerStatus = false;
+
+ {
+ var driver = driverFor(testFile1);
+ var collector = DriverEventCollector(driver, idProvider: idProvider);
+
+ var libraryUri = Uri.parse('package:bar/bar.dart');
+ collector.getLibraryByUri('T1', '$libraryUri');
+
+ await assertEventsText(collector, r'''
+[operation] linkLibraryCycle SDK
+[operation] linkLibraryCycle
+ package:foo/foo.dart
+ declaredClasses
+ A: #M0
+ interface: #M1
+ exportMapId: #M2
+ exportMap
+ A: #M0
+[operation] linkLibraryCycle
+ package:bar/bar.dart
+ declaredClasses
+ B: #M3
+ interface: #M4
+ exportMapId: #M5
+ exportMap
+ B: #M3
+''');
+ }
+
+ {
+ var driver = driverFor(testFile2);
+ var collector = DriverEventCollector(driver, idProvider: idProvider);
+
+ var libraryUri = Uri.parse('package:bar/bar.dart');
+ collector.getLibraryByUri('T2', '$libraryUri');
+
+ // Note: new library manifests.
+ // We don't try to reuse `package:bar` library bundle.
+ // Because URI resolution is different in T2, keys are different.
+ await assertEventsText(collector, r'''
+[operation] reuseLinkedBundle SDK
+[operation] linkLibraryCycle
+ package:foo/foo.dart
+ declaredClasses
+ A: #M6
+ interface: #M7
+ A2: #M8
+ interface: #M9
+ exportMapId: #M10
+ exportMap
+ A: #M6
+ A2: #M8
+[operation] linkLibraryCycle
+ package:bar/bar.dart
+ declaredClasses
+ B: #M11
+ interface: #M12
+ exportMapId: #M13
+ exportMap
+ B: #M11
+''');
+ }
+
+ await disposeAnalysisContextCollection();
+
+ {
+ var driver = driverFor(testFile1);
+ var collector = DriverEventCollector(driver, idProvider: idProvider);
+
+ var libraryUri = Uri.parse('package:bar/bar.dart');
+ collector.getLibraryByUri('T3', '$libraryUri');
+
+ // We used different keys in T1 and T2, so both can be used.
+ // This is important, because they use different `package:foo`.
+ await assertEventsText(collector, r'''
+[operation] reuseLinkedBundle SDK
+[operation] reuseLinkedBundle
+ package:foo/foo.dart
+[operation] reuseLinkedBundle
+ package:bar/bar.dart
+''');
+ }
+
+ {
+ var driver = driverFor(testFile2);
+ var collector = DriverEventCollector(driver, idProvider: idProvider);
+
+ var libraryUri = Uri.parse('package:bar/bar.dart');
+ collector.getLibraryByUri('T4', '$libraryUri');
+
+ // We used different keys in T1 and T2, so both can be used.
+ // This is important, because they use different `package:foo`.
+ await assertEventsText(collector, r'''
+[operation] reuseLinkedBundle SDK
+[operation] reuseLinkedBundle
+ package:foo/foo.dart
+[operation] reuseLinkedBundle
+ package:bar/bar.dart
+''');
+ }
+ }
+
+ test_manifest_key_samePackages() async {
+ newFile('/packages/foo/lib/foo.dart', r'''
+class A {}
+''');
+
+ newFile('/packages/bar/lib/bar.dart', r'''
+import 'package:foo/foo.dart';
+class B extends A {}
+''');
+
+ writePackageConfig(
+ '$workspaceRootPath/test_1',
+ PackageConfigFileBuilder()
+ ..add(name: 'foo', rootPath: '/packages/foo')
+ ..add(name: 'test', rootPath: '$workspaceRootPath/test_1'),
+ );
+
+ writePackageConfig(
+ '$workspaceRootPath/test_2',
+ PackageConfigFileBuilder()
+ ..add(name: 'foo', rootPath: '/packages/foo')
+ ..add(name: 'bar', rootPath: '/packages/bar')
+ ..add(name: 'test', rootPath: '$workspaceRootPath/test_2'),
+ );
+
+ var testFile1 = newFile('$workspaceRootPath/test_1/lib/test.dart', r'''
+import 'package:foo/foo.dart';
+''');
+
+ var testFile2 = newFile('$workspaceRootPath/test_2/lib/test.dart', r'''
+import 'package:foo/foo.dart';
+import 'package:bar/bar.dart';
+''');
+
+ configuration
+ ..withGetLibraryByUri = false
+ ..withLibraryManifest = true
+ ..withLinkLibraryCycle = true
+ ..withSchedulerStatus = false;
+
+ {
+ var driver = driverFor(testFile1);
+ var collector = DriverEventCollector(driver, idProvider: idProvider);
+
+ var libraryUri = Uri.parse('package:test/test.dart');
+ collector.getLibraryByUri('T1', '$libraryUri');
+
+ await assertEventsText(collector, r'''
+[operation] linkLibraryCycle SDK
+[operation] linkLibraryCycle
+ package:foo/foo.dart
+ declaredClasses
+ A: #M0
+ interface: #M1
+ exportMapId: #M2
+ exportMap
+ A: #M0
+[operation] linkLibraryCycle
+ package:test/test.dart
+ exportMapId: #M3
+''');
+ }
+
+ {
+ var driver = driverFor(testFile2);
+ var collector = DriverEventCollector(driver, idProvider: idProvider);
+
+ var libraryUri = Uri.parse('package:test/test.dart');
+ collector.getLibraryByUri('T2', '$libraryUri');
+
+ // Note, we reuse `package:foo`, even though T2 has different total
+ // URI resolution rules. But for `package:foo` everything it the same.
+ await assertEventsText(collector, r'''
+[operation] reuseLinkedBundle SDK
+[operation] reuseLinkedBundle
+ package:foo/foo.dart
+[operation] linkLibraryCycle
+ package:bar/bar.dart
+ declaredClasses
+ B: #M4
+ interface: #M5
+ exportMapId: #M6
+ exportMap
+ B: #M4
+[operation] linkLibraryCycle
+ package:test/test.dart
+ exportMapId: #M7
+''');
+ }
+ }
+
test_manifest_library_metadata_add() async {
await _runLibraryManifestScenario(
initialCode: r'''