static (ImmutableArray <IDependency> TopLevelDependencies, Dictionary <string, IDependency> topLevelDependencyByPath, bool hasVisibleUnresolvedDependency) Scan(ImmutableDictionary <string, IDependency> dependenciesWorld, ITargetFramework targetFramework)
            {
                // TODO use ToImmutableAndFree?
                ImmutableArray <IDependency> .Builder topLevelDependencies = ImmutableArray.CreateBuilder <IDependency>();

                bool hasVisibleUnresolvedDependency = false;
                var  topLevelDependencyByPath       = new Dictionary <string, IDependency>(StringComparer.OrdinalIgnoreCase);

                foreach ((string id, IDependency dependency) in dependenciesWorld)
                {
                    System.Diagnostics.Debug.Assert(
                        string.Equals(id, dependency.Id),
                        "dependenciesWorld dictionary entry keys must match their value's ids.");

                    if (!dependency.Resolved && dependency.Visible)
                    {
                        hasVisibleUnresolvedDependency = true;
                    }

                    if (dependency.TopLevel)
                    {
                        topLevelDependencies.Add(dependency);

                        if (!string.IsNullOrEmpty(dependency.Path))
                        {
                            topLevelDependencyByPath.Add(
                                Dependency.GetID(targetFramework, dependency.ProviderType, dependency.Path),
                                dependency);
                        }
                    }
                }

                return(topLevelDependencies.ToImmutable(), topLevelDependencyByPath, hasVisibleUnresolvedDependency);
            }
Exemple #2
0
        public void GetID_ThrowsForInvalidArguments()
        {
            Assert.Throws <ArgumentNullException>(() => Dependency.GetID(null, "providerType", "modelId"));
            Assert.Throws <ArgumentNullException>(() => Dependency.GetID(TargetFramework.Any, null, "modelId"));
            Assert.Throws <ArgumentNullException>(() => Dependency.GetID(TargetFramework.Any, "providerType", null));

            Assert.Throws <ArgumentException>(() => Dependency.GetID(TargetFramework.Any, "", "modelId"));
            Assert.Throws <ArgumentException>(() => Dependency.GetID(TargetFramework.Any, "providerType", ""));
        }
        public void FromChanges_NoChangesAfterBeforeRemoveFilterDeclinedChange()
        {
            const string projectPath = @"c:\somefolder\someproject\a.csproj";

            var targetFramework = new TargetFramework("tfm1");

            var dependencyTop1 = new TestDependency
            {
                ProviderType   = "Xxx",
                Id             = Dependency.GetID(targetFramework, "Xxx", "topdependency1"),
                Name           = "TopDependency1",
                Caption        = "TopDependency1",
                SchemaItemType = "Xxx",
                Resolved       = true,
                TopLevel       = true
            };

            var dependencyChild1 = new TestDependency
            {
                ProviderType   = "Xxx",
                Id             = Dependency.GetID(targetFramework, "Xxx", "childdependency1"),
                Name           = "ChildDependency1",
                Caption        = "ChildDependency1",
                SchemaItemType = "Xxx",
                Resolved       = true,
                TopLevel       = false
            };

            var catalogs         = IProjectCatalogSnapshotFactory.Create();
            var previousSnapshot = new TargetedDependenciesSnapshot(
                projectPath,
                targetFramework,
                catalogs,
                new IDependency[] { dependencyTop1, dependencyChild1 }.ToImmutableDictionary(d => d.Id).WithComparers(StringComparer.OrdinalIgnoreCase));

            var changes = new DependenciesChangesBuilder();

            changes.Removed(dependencyTop1.ProviderType, dependencyTop1.Id);

            var snapshotFilter = new TestDependenciesSnapshotFilter();

            var snapshot = TargetedDependenciesSnapshot.FromChanges(
                projectPath,
                previousSnapshot,
                changes.TryBuildChanges() !,
                catalogs,
                ImmutableArray.Create <IDependenciesSnapshotFilter>(snapshotFilter),
                new Dictionary <string, IProjectDependenciesSubTreeProvider>(),
                null);

            Assert.Same(previousSnapshot, snapshot);
            Assert.True(snapshotFilter.Completed);
        }
Exemple #4
0
        public void FromChanges_NoChangesAfterBeforeRemoveFilterDeclinedChange()
        {
            const string projectPath = @"c:\somefolder\someproject\a.csproj";

            var targetFramework = new TargetFramework("tfm1");

            var dependencyTop1 = new TestDependency
            {
                ProviderType   = "Xxx",
                Id             = Dependency.GetID(targetFramework, "Xxx", "topdependency1"),
                Name           = "TopDependency1",
                Caption        = "TopDependency1",
                SchemaItemType = "Xxx",
                Resolved       = true,
                TopLevel       = true
            };

            var dependencyChild1 = new TestDependency
            {
                ProviderType   = "Xxx",
                Id             = Dependency.GetID(targetFramework, "Xxx", "childdependency1"),
                Name           = "ChildDependency1",
                Caption        = "ChildDependency1",
                SchemaItemType = "Xxx",
                Resolved       = true,
                TopLevel       = false
            };

            var catalogs         = IProjectCatalogSnapshotFactory.Create();
            var previousSnapshot = ITargetedDependenciesSnapshotFactory.Implement(
                projectPath: projectPath,
                targetFramework: targetFramework,
                catalogs: catalogs,
                dependenciesWorld: new [] { dependencyTop1, dependencyChild1 },
                topLevelDependencies: new [] { dependencyTop1 });

            var changes = new DependenciesChangesBuilder();

            changes.Removed(dependencyTop1.ProviderType, dependencyTop1.Id);

            var snapshotFilter = new TestDependenciesSnapshotFilter();

            var snapshot = TargetedDependenciesSnapshot.FromChanges(
                projectPath,
                previousSnapshot,
                changes.Build(),
                catalogs,
                ImmutableArray.Create <IDependenciesSnapshotFilter>(snapshotFilter),
                new Dictionary <string, IProjectDependenciesSubTreeProvider>(),
                null);

            Assert.Same(previousSnapshot, snapshot);
        }
 private void ConstructTopLevelDependenciesByPathMap()
 {
     foreach (var topLevelDependency in TopLevelDependencies)
     {
         if (!string.IsNullOrEmpty(topLevelDependency.Path))
         {
             _topLevelDependenciesByPathMap.Add(
                 Dependency.GetID(TargetFramework, topLevelDependency.ProviderType, topLevelDependency.Path),
                 topLevelDependency);
         }
     }
 }
        public void BeforeAddOrUpdate_WhenSdk_ShouldFindMatchingPackageAndSetProperties()
        {
            var dependencyIDs = ImmutableArray.Create("id1", "id2");

            var targetFramework = new TargetFramework("tfm");

            const string sdkName = "sdkName";

            var sdkDependency = new TestDependency
            {
                Id       = "dependency1Id",
                Name     = sdkName,
                TopLevel = true,
                Resolved = false,
                Flags    = DependencyTreeFlags.SdkDependency
            };

            var packageDependency = new TestDependency
            {
                Id            = Dependency.GetID(targetFramework, PackageRuleHandler.ProviderTypeString, sdkName),
                Resolved      = true,
                DependencyIDs = dependencyIDs,
                Flags         = DependencyTreeFlags.NuGetPackageDependency
            };

            var worldBuilder = new IDependency[] { sdkDependency, packageDependency }.ToImmutableDictionary(d => d.Id).ToBuilder();

            var context = new AddDependencyContext(worldBuilder);

            var filter = new SdkAndPackagesDependenciesSnapshotFilter();

            filter.BeforeAddOrUpdate(
                targetFramework,
                sdkDependency,
                null,
                null,
                context);

            var acceptedDependency = context.GetResult(filter);

            // Dependency should be accepted, but converted to resolved state
            Assert.NotNull(acceptedDependency);
            Assert.NotSame(sdkDependency, acceptedDependency);
            acceptedDependency.AssertEqualTo(
                sdkDependency.ToResolved(
                    schemaName: ResolvedSdkReference.SchemaName,
                    dependencyIDs: dependencyIDs));

            // No changes other than the filtered dependency
            Assert.False(context.Changed);
        }
        public void BeforeAddOrUpdate_WhenPackage_ShouldFindMatchingSdkAndSetProperties()
        {
            var dependencyIDs = ImmutableArray.Create("id1", "id2");

            var targetFramework = new TargetFramework("tfm");

            const string packageName = "packageName";

            var sdkDependency = new TestDependency
            {
                Id       = Dependency.GetID(targetFramework, SdkRuleHandler.ProviderTypeString, packageName),
                TopLevel = false,
                Resolved = true,
                Flags    = DependencyTreeFlags.NuGetPackageDependency.Union(DependencyTreeFlags.Unresolved) // to see if unresolved is fixed
            };

            var packageDependency = new TestDependency
            {
                Id            = "packageId",
                Name          = packageName,
                Flags         = DependencyTreeFlags.NuGetPackageDependency,
                TopLevel      = true,
                Resolved      = true,
                DependencyIDs = dependencyIDs
            };

            var worldBuilder = new IDependency[] { packageDependency, sdkDependency }.ToImmutableDictionary(d => d.Id).ToBuilder();

            var context = new AddDependencyContext(worldBuilder);

            var filter = new SdkAndPackagesDependenciesSnapshotFilter();

            filter.BeforeAddOrUpdate(
                targetFramework,
                packageDependency,
                null,
                null,
                context);

            // Accepts unchanged dependency
            Assert.Same(packageDependency, context.GetResult(filter));

            // Other changes made
            Assert.True(context.Changed);

            Assert.True(context.TryGetDependency(sdkDependency.Id, out IDependency sdkDependencyAfter));
            sdkDependencyAfter.AssertEqualTo(
                sdkDependency.ToResolved(
                    schemaName: ResolvedSdkReference.SchemaName,
                    dependencyIDs: dependencyIDs));
        }
        public void BeforeRemove_WhenPackageRemoving_ShouldCleanupSdk()
        {
            const string packageName = "packageName";

            var targetFramework = new TargetFramework("tfm");

            var sdkDependency = new TestDependency
            {
                Id       = Dependency.GetID(targetFramework, SdkRuleHandler.ProviderTypeString, packageName),
                TopLevel = false,
                Resolved = true,
                Flags    = DependencyTreeFlags.SdkDependency.Union(DependencyTreeFlags.Resolved)
            };

            var packageDependency = new TestDependency
            {
                Id       = "packageId",
                Name     = packageName,
                Flags    = DependencyTreeFlags.NuGetPackageDependency,
                TopLevel = true,
                Resolved = true
            };

            var worldBuilder = new IDependency[] { packageDependency, sdkDependency }.ToImmutableDictionary(d => d.Id).ToBuilder();

            var context = new RemoveDependencyContext(worldBuilder);

            var filter = new SdkAndPackagesDependenciesSnapshotFilter();

            filter.BeforeRemove(
                targetFramework: targetFramework,
                dependency: packageDependency,
                context);

            // Accepts removal
            Assert.True(context.GetResult(filter));

            // Makes other changes too
            Assert.True(context.Changed);

            Assert.True(worldBuilder.TryGetValue(packageDependency.Id, out var afterPackageDependency));
            Assert.Same(packageDependency, afterPackageDependency);

            Assert.True(worldBuilder.TryGetValue(sdkDependency.Id, out var afterSdkDependency));
            afterSdkDependency.AssertEqualTo(
                afterSdkDependency.ToUnresolved(
                    SdkReference.SchemaName,
                    dependencyIDs: ImmutableArray <string> .Empty));
        }
Exemple #9
0
        // Internal, for test use -- normal code should use the factory methods
        internal TargetedDependenciesSnapshot(
            string projectPath,
            ITargetFramework targetFramework,
            IProjectCatalogSnapshot catalogs,
            ImmutableDictionary <string, IDependency> dependenciesWorld)
        {
            Requires.NotNullOrEmpty(projectPath, nameof(projectPath));
            Requires.NotNull(targetFramework, nameof(targetFramework));
            // catalogs can be null
            Requires.NotNull(dependenciesWorld, nameof(dependenciesWorld));

            ProjectPath       = projectPath;
            TargetFramework   = targetFramework;
            Catalogs          = catalogs;
            DependenciesWorld = dependenciesWorld;

            bool hasUnresolvedDependency = false;

            ImmutableHashSet <IDependency> .Builder topLevelDependencies = ImmutableHashSet.CreateBuilder <IDependency>();

            foreach ((string id, IDependency dependency) in dependenciesWorld)
            {
                System.Diagnostics.Debug.Assert(
                    string.Equals(id, dependency.Id),
                    "dependenciesWorld dictionary entry keys must match their value's ids.");

                if (!dependency.Resolved)
                {
                    hasUnresolvedDependency = true;
                }

                if (dependency.TopLevel)
                {
                    bool added = topLevelDependencies.Add(dependency);
                    System.Diagnostics.Debug.Assert(added, "Duplicate top level dependency found.");

                    if (!string.IsNullOrEmpty(dependency.Path))
                    {
                        _topLevelDependenciesByPathMap.Add(
                            Dependency.GetID(TargetFramework, dependency.ProviderType, dependency.Path),
                            dependency);
                    }
                }
            }

            HasUnresolvedDependency = hasUnresolvedDependency;
            TopLevelDependencies    = topLevelDependencies.ToImmutable();
        }
        public void BeforeAddOrUpdate_WhenSdkAndPackageUnresolved_ShouldDoNothing()
        {
            var targetFramework = new TargetFramework("tfm");

            const string sdkName = "sdkName";

            var sdkDependency = new TestDependency
            {
                Id       = "dependency1",
                Name     = sdkName,
                TopLevel = false,
                Resolved = true,
                Flags    = DependencyTreeFlags.SdkDependency
            };

            var packageDependency = new TestDependency
            {
                Id       = Dependency.GetID(targetFramework, PackageRuleHandler.ProviderTypeString, sdkName),
                Resolved = false,
                Flags    = DependencyTreeFlags.NuGetPackageDependency
            };

            var worldBuilder = new IDependency[] { sdkDependency, packageDependency }.ToImmutableDictionary(d => d.Id).ToBuilder();

            var context = new AddDependencyContext(worldBuilder);

            var filter = new SdkAndPackagesDependenciesSnapshotFilter();

            filter.BeforeAddOrUpdate(
                targetFramework,
                sdkDependency,
                null,
                null,
                context);

            // Accepts unchanged dependency
            Assert.Same(sdkDependency, context.GetResult(filter));

            // No other changes made
            Assert.False(context.Changed);
        }
Exemple #11
0
 public void GetID_CreatesCorrectString(string modelId, string expected)
 {
     Assert.Equal(expected, Dependency.GetID(TargetFramework.Any, "providerType", modelId));
 }
 /// <summary>
 /// Returns id having full path instead of OriginalItemSpec
 /// </summary>
 public static string GetTopLevelId(this IDependency self)
 {
     return(string.IsNullOrEmpty(self.Path)
         ? self.Id
         : Dependency.GetID(self.TargetFramework, self.ProviderType, self.Path));
 }
        /// <summary>
        /// Applies changes to <paramref name="previousSnapshot"/> and produces a new snapshot if required.
        /// If no changes are made, <paramref name="previousSnapshot"/> is returned unmodified.
        /// </summary>
        /// <returns>An updated snapshot, or <paramref name="previousSnapshot"/> if no changes occured.</returns>
        public static TargetedDependenciesSnapshot FromChanges(
            string projectPath,
            TargetedDependenciesSnapshot previousSnapshot,
            IDependenciesChanges changes,
            IProjectCatalogSnapshot?catalogs,
            ImmutableArray <IDependenciesSnapshotFilter> snapshotFilters,
            IReadOnlyDictionary <string, IProjectDependenciesSubTreeProvider> subTreeProviderByProviderType,
            IImmutableSet <string>?projectItemSpecs)
        {
            Requires.NotNullOrWhiteSpace(projectPath, nameof(projectPath));
            Requires.NotNull(previousSnapshot, nameof(previousSnapshot));
            Requires.NotNull(changes, nameof(changes));
            Requires.Argument(!snapshotFilters.IsDefault, nameof(snapshotFilters), "Cannot be default.");
            Requires.NotNull(subTreeProviderByProviderType, nameof(subTreeProviderByProviderType));

            bool anyChanges = false;

            ITargetFramework targetFramework = previousSnapshot.TargetFramework;

            var worldBuilder = previousSnapshot.DependenciesWorld.ToBuilder();

            if (changes.RemovedNodes.Count != 0)
            {
                var context = new RemoveDependencyContext(worldBuilder);

                foreach (IDependencyModel removed in changes.RemovedNodes)
                {
                    Remove(context, removed);
                }
            }

            if (changes.AddedNodes.Count != 0)
            {
                var context = new AddDependencyContext(worldBuilder);

                foreach (IDependencyModel added in changes.AddedNodes)
                {
                    Add(context, added);
                }
            }

            // Also factor in any changes to path/framework/catalogs
            anyChanges =
                anyChanges ||
                !StringComparers.Paths.Equals(projectPath, previousSnapshot.ProjectPath) ||
                !targetFramework.Equals(previousSnapshot.TargetFramework) ||
                !Equals(catalogs, previousSnapshot.Catalogs);

            if (anyChanges)
            {
                return(new TargetedDependenciesSnapshot(
                           projectPath,
                           targetFramework,
                           catalogs,
                           worldBuilder.ToImmutable()));
            }

            return(previousSnapshot);

            void Remove(RemoveDependencyContext context, IDependencyModel dependencyModel)
            {
                string dependencyId = Dependency.GetID(
                    targetFramework, dependencyModel.ProviderType, dependencyModel.Id);

                if (!context.TryGetDependency(dependencyId, out IDependency dependency))
                {
                    return;
                }

                context.Reset();

                foreach (IDependenciesSnapshotFilter filter in snapshotFilters)
                {
                    filter.BeforeRemove(
                        targetFramework,
                        dependency,
                        context);

                    anyChanges |= context.Changed;

                    if (!context.GetResult(filter))
                    {
                        // TODO breaking here denies later filters the opportunity to modify builders
                        return;
                    }
                }

                worldBuilder.Remove(dependencyId);
                anyChanges = true;
            }

            void Add(AddDependencyContext context, IDependencyModel dependencyModel)
            {
                // Create the unfiltered dependency
                IDependency?dependency = new Dependency(dependencyModel, targetFramework, projectPath);

                context.Reset();

                foreach (IDependenciesSnapshotFilter filter in snapshotFilters)
                {
                    filter.BeforeAddOrUpdate(
                        targetFramework,
                        dependency,
                        subTreeProviderByProviderType,
                        projectItemSpecs,
                        context);

                    dependency = context.GetResult(filter);

                    if (dependency == null)
                    {
                        break;
                    }
                }

                if (dependency != null)
                {
                    // A dependency was accepted
                    worldBuilder.Remove(dependency.Id);
                    worldBuilder.Add(dependency.Id, dependency);
                    anyChanges = true;
                }
                else
                {
                    // Even though the dependency was rejected, it's possible that filters made
                    // changes to other dependencies.
                    anyChanges |= context.Changed;
                }
            }
        }
        private bool MergeChanges(
            IDependenciesChanges changes,
            IEnumerable <IDependenciesSnapshotFilter> snapshotFilters)
        {
            var worldBuilder = ImmutableDictionary.CreateBuilder <string, IDependency>(
                StringComparer.OrdinalIgnoreCase);

            worldBuilder.AddRange(DependenciesWorld);
            var topLevelBuilder = ImmutableHashSet.CreateBuilder <IDependency>();

            topLevelBuilder.AddRange(TopLevelDependencies);

            var anyChanges = false;

            foreach (var removed in changes.RemovedNodes)
            {
                var targetedId = Dependency.GetID(TargetFramework, removed.ProviderType, removed.Id);
                if (!worldBuilder.TryGetValue(targetedId, out IDependency dependency))
                {
                    continue;
                }

                if (snapshotFilters != null)
                {
                    foreach (var filter in snapshotFilters)
                    {
                        dependency = filter.BeforeRemove(
                            ProjectPath, TargetFramework, dependency, worldBuilder, topLevelBuilder, out bool filterAnyChanges);

                        anyChanges |= filterAnyChanges;

                        if (dependency == null)
                        {
                            break;
                        }
                    }
                }

                if (dependency == null)
                {
                    continue;
                }

                anyChanges = true;

                worldBuilder.Remove(targetedId);
                topLevelBuilder.Remove(dependency);
            }

            foreach (var added in changes.AddedNodes)
            {
                IDependency newDependency = new Dependency(added, TargetFramework);

                if (snapshotFilters != null)
                {
                    foreach (var filter in snapshotFilters)
                    {
                        newDependency = filter.BeforeAdd(
                            ProjectPath, TargetFramework, newDependency, worldBuilder, topLevelBuilder, out bool filterAnyChanges);

                        anyChanges |= filterAnyChanges;

                        if (newDependency == null)
                        {
                            break;
                        }
                    }
                }

                if (newDependency == null)
                {
                    continue;
                }

                anyChanges = true;

                worldBuilder.Remove(newDependency.Id);
                worldBuilder.Add(newDependency.Id, newDependency);
                if (newDependency.TopLevel)
                {
                    topLevelBuilder.Remove(newDependency);
                    topLevelBuilder.Add(newDependency);
                }
            }

            DependenciesWorld    = worldBuilder.ToImmutable();
            TopLevelDependencies = topLevelBuilder.ToImmutable();

            ConstructTopLevelDependenciesByPathMap();

            return(anyChanges);
        }
Exemple #15
0
        private bool MergeChanges(
            IDependenciesChanges changes,
            IEnumerable <IDependenciesSnapshotFilter> snapshotFilters,
            IEnumerable <IProjectDependenciesSubTreeProvider> subTreeProviders,
            HashSet <string> projectItemSpecs)
        {
            var worldBuilder    = DependenciesWorld.ToBuilder();
            var topLevelBuilder = TopLevelDependencies.ToBuilder();

            var anyChanges = false;

            foreach (var removed in changes.RemovedNodes)
            {
                var targetedId = Dependency.GetID(TargetFramework, removed.ProviderType, removed.Id);
                if (!worldBuilder.TryGetValue(targetedId, out IDependency dependency))
                {
                    continue;
                }

                if (snapshotFilters != null)
                {
                    foreach (var filter in snapshotFilters)
                    {
                        dependency = filter.BeforeRemove(
                            ProjectPath, TargetFramework, dependency, worldBuilder, topLevelBuilder, out bool filterAnyChanges);

                        anyChanges |= filterAnyChanges;

                        if (dependency == null)
                        {
                            break;
                        }
                    }
                }

                if (dependency == null)
                {
                    continue;
                }

                anyChanges = true;

                worldBuilder.Remove(targetedId);
                topLevelBuilder.Remove(dependency);
            }

            var subTreeProvidersMap = GetSubTreeProviderMap(subTreeProviders);

            foreach (var added in changes.AddedNodes)
            {
                IDependency newDependency = new Dependency(added, TargetFramework, ProjectPath);

                if (snapshotFilters != null)
                {
                    foreach (var filter in snapshotFilters)
                    {
                        newDependency = filter.BeforeAdd(
                            ProjectPath,
                            TargetFramework,
                            newDependency,
                            worldBuilder,
                            topLevelBuilder,
                            subTreeProvidersMap,
                            projectItemSpecs,
                            out bool filterAnyChanges);

                        anyChanges |= filterAnyChanges;

                        if (newDependency == null)
                        {
                            break;
                        }
                    }
                }

                if (newDependency == null)
                {
                    continue;
                }

                anyChanges = true;

                worldBuilder.Remove(newDependency.Id);
                worldBuilder.Add(newDependency.Id, newDependency);
                if (newDependency.TopLevel)
                {
                    topLevelBuilder.Remove(newDependency);
                    topLevelBuilder.Add(newDependency);
                }
            }

            DependenciesWorld    = worldBuilder.ToImmutable();
            TopLevelDependencies = topLevelBuilder.ToImmutable();

            ConstructTopLevelDependenciesByPathMap();

            return(anyChanges);
        }
Exemple #16
0
        /// <summary>
        /// Applies changes to <paramref name="previousSnapshot"/> and produces a new snapshot if required.
        /// If no changes are made, <paramref name="previousSnapshot"/> is returned unmodified.
        /// </summary>
        /// <returns>An updated snapshot, or <paramref name="previousSnapshot"/> if no changes occured.</returns>
        public static ITargetedDependenciesSnapshot FromChanges(
            string projectPath,
            ITargetedDependenciesSnapshot previousSnapshot,
            IDependenciesChanges changes,
            IProjectCatalogSnapshot catalogs,
            IReadOnlyList <IDependenciesSnapshotFilter> snapshotFilters,
            IReadOnlyDictionary <string, IProjectDependenciesSubTreeProvider> subTreeProviderByProviderType,
            IImmutableSet <string> projectItemSpecs)
        {
            Requires.NotNullOrWhiteSpace(projectPath, nameof(projectPath));
            Requires.NotNull(previousSnapshot, nameof(previousSnapshot));
            // catalogs can be null
            Requires.NotNull(changes, nameof(changes));
            Requires.NotNull(snapshotFilters, nameof(snapshotFilters));
            Requires.NotNull(subTreeProviderByProviderType, nameof(subTreeProviderByProviderType));
            // projectItemSpecs can be null

            bool anyChanges = false;

            ITargetFramework targetFramework = previousSnapshot.TargetFramework;

            var worldBuilder = previousSnapshot.DependenciesWorld.ToBuilder();

            foreach (IDependencyModel removedNode in changes.RemovedNodes)
            {
                string targetedId = Dependency.GetID(targetFramework, removedNode.ProviderType, removedNode.Id);

                if (!worldBuilder.TryGetValue(targetedId, out IDependency dependency))
                {
                    continue;
                }

                bool canRemove = true;

                if (snapshotFilters != null)
                {
                    foreach (IDependenciesSnapshotFilter filter in snapshotFilters)
                    {
                        canRemove = filter.BeforeRemove(
                            projectPath, targetFramework, dependency, worldBuilder, out bool filterAnyChanges);

                        anyChanges |= filterAnyChanges;

                        if (!canRemove)
                        {
                            // TODO breaking here denies later filters the opportunity to modify builders
                            break;
                        }
                    }
                }

                if (canRemove)
                {
                    anyChanges = true;
                    worldBuilder.Remove(targetedId);
                }
            }

            foreach (IDependencyModel added in changes.AddedNodes)
            {
                IDependency newDependency = new Dependency(added, targetFramework, projectPath);

                if (snapshotFilters != null)
                {
                    foreach (IDependenciesSnapshotFilter filter in snapshotFilters)
                    {
                        newDependency = filter.BeforeAdd(
                            projectPath,
                            targetFramework,
                            newDependency,
                            worldBuilder,
                            subTreeProviderByProviderType,
                            projectItemSpecs,
                            out bool filterAnyChanges);

                        anyChanges |= filterAnyChanges;

                        if (newDependency == null)
                        {
                            break;
                        }
                    }
                }

                if (newDependency == null)
                {
                    continue;
                }

                anyChanges = true;

                worldBuilder.Remove(newDependency.Id);
                worldBuilder.Add(newDependency.Id, newDependency);
            }

            // Also factor in any changes to path/framework/catalogs
            anyChanges =
                anyChanges ||
                !StringComparers.Paths.Equals(projectPath, previousSnapshot.ProjectPath) ||
                !targetFramework.Equals(previousSnapshot.TargetFramework) ||
                !Equals(catalogs, previousSnapshot.Catalogs);

            if (anyChanges)
            {
                return(new TargetedDependenciesSnapshot(
                           projectPath,
                           targetFramework,
                           catalogs,
                           worldBuilder.ToImmutable()));
            }

            return(previousSnapshot);
        }