File: UnusedReferences\UnusedReferencesRemoverTests.cs
Web Access
Project: src\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.UnusedReferences;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.UnitTests.UnusedReferences;
 
[Trait(Traits.Feature, Traits.Features.UnusedReferences)]
public class UnusedReferencesRemoverTests
{
    private static readonly string[] Empty = [];
 
    private const string UsedAssemblyName = "Used.dll";
    private const string UsedAssemblyPath = $"/libs/{UsedAssemblyName}";
    private const string UnusedAssemblyName = "Unused.dll";
    private const string UnusedAssemblyPath = $"/libs/{UnusedAssemblyName}";
 
    [Fact]
    public void GetUnusedReferences_DirectlyUsedAssemblyReferences_AreNotReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var usedReference = AssemblyReference(UsedAssemblyPath);
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, usedReference);
 
        Assert.Empty(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_DirectlyUsedPackageReferences_AreNotReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var usedReference = PackageReference(UsedAssemblyPath);
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, usedReference);
 
        Assert.Empty(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_DirectlyUsedProjectReferences_AreNotReturned()
    {
        var usedProjects = new[] { UsedAssemblyName };
        var usedReference = ProjectReference(UsedAssemblyName);
 
        var unusedReferences = GetUnusedReferences(usedCompilationAssemblies: Empty, usedProjects, usedReference);
 
        Assert.Empty(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_UnusedReferences_AreReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var unusedReference = PackageReference(UnusedAssemblyPath);
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, unusedReference);
 
        Assert.Contains(unusedReference, unusedReferences);
        Assert.Single(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_TransitivelyUsedPackageReferences_AreNotReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var transitivelyUsedReference = PackageReference(UnusedAssemblyName, PackageReference(UsedAssemblyPath));
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, transitivelyUsedReference);
 
        Assert.Empty(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_TransitivelyUsedProjectReferences_AreNotReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var transitivelyUsedReference = ProjectReference(UnusedAssemblyName, PackageReference(UsedAssemblyPath));
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, transitivelyUsedReference);
 
        Assert.Empty(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_WhenUsedAssemblyIsAvilableDirectlyAndTransitively_DirectReferencesAreReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var transitivelyUsedReference = ProjectReference(UnusedAssemblyPath, PackageReference(UsedAssemblyPath));
        var directlyUsedReference = PackageReference(UsedAssemblyPath);
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, transitivelyUsedReference, directlyUsedReference);
 
        Assert.Contains(transitivelyUsedReference, unusedReferences);
        Assert.Single(unusedReferences);
    }
 
    [Fact]
    public void GetUnusedReferences_ReferencesThatDoNotContributeToCompilation_AreNotReturned()
    {
        var usedAssemblies = new[] { UsedAssemblyPath };
        var analyzerReference = new ReferenceInfo(
            ReferenceType.Package,
            itemSpecification: "Analyzer",
            treatAsUsed: false,
            compilationAssemblies: ImmutableArray<string>.Empty,
            dependencies: ImmutableArray<ReferenceInfo>.Empty);
 
        var unusedReferences = GetUnusedReferences(usedAssemblies, usedProjectAssemblyNames: Empty, analyzerReference);
 
        Assert.Empty(unusedReferences);
    }
 
    [Theory]
    [InlineData(UpdateAction.None, false)]
    [InlineData(UpdateAction.None, true)]
    [InlineData(UpdateAction.TreatAsUnused, false)]
    [InlineData(UpdateAction.TreatAsUsed, true)]
    internal async Task ApplyReferenceUpdates_NoChangeUpdates_AreNotApplied(UpdateAction action, bool treatAsUsed)
    {
        var noChangeUpdate = new ReferenceUpdate(action, PackageReference(UnusedAssemblyPath, treatAsUsed));
 
        var appliedUpdates = await ApplyReferenceUpdatesAsync(noChangeUpdate);
 
        Assert.Empty(appliedUpdates);
    }
 
    [Theory]
    [InlineData(UpdateAction.Remove, false)]
    [InlineData(UpdateAction.Remove, true)]
    [InlineData(UpdateAction.TreatAsUnused, true)]
    [InlineData(UpdateAction.TreatAsUsed, false)]
    internal async Task ApplyReferenceUpdates_ChangeUpdates_AreApplied(UpdateAction action, bool treatAsUsed)
    {
        var changeUpdate = new ReferenceUpdate(action, PackageReference(UnusedAssemblyPath, treatAsUsed));
 
        var appliedUpdates = await ApplyReferenceUpdatesAsync(changeUpdate);
 
        Assert.Contains(changeUpdate, appliedUpdates);
        Assert.Single(appliedUpdates);
    }
 
    [Fact]
    public async Task ApplyReferenceUpdates_MixOfChangeAndNoChangeUpdates_ChangesAreApplied()
    {
        var noChangeUpdate = new ReferenceUpdate(UpdateAction.None, PackageReference(UsedAssemblyPath));
        var changeUpdate = new ReferenceUpdate(UpdateAction.Remove, PackageReference(UnusedAssemblyPath));
 
        var appliedUpdates = await ApplyReferenceUpdatesAsync(noChangeUpdate, changeUpdate);
 
        Assert.Contains(changeUpdate, appliedUpdates);
        Assert.Single(appliedUpdates);
    }
 
    private static ImmutableArray<ReferenceInfo> GetUnusedReferences(string[] usedCompilationAssemblies, string[] usedProjectAssemblyNames, params ReferenceInfo[] references)
        => UnusedReferencesRemover.GetUnusedReferences(new(usedCompilationAssemblies), new(usedProjectAssemblyNames), references.ToImmutableArray());
 
    private static async Task<ImmutableArray<ReferenceUpdate>> ApplyReferenceUpdatesAsync(params ReferenceUpdate[] referenceUpdates)
    {
        var referenceCleanupService = new TestReferenceCleanupService();
 
        await UnusedReferencesRemover.ApplyReferenceUpdatesAsync(
            referenceCleanupService,
            string.Empty,
            referenceUpdates.ToImmutableArray(),
            CancellationToken.None).ConfigureAwait(false);
 
        return referenceCleanupService.AppliedUpdates.ToImmutableArray();
    }
 
    private static ReferenceInfo ProjectReference(string assemblyPath, params ReferenceInfo[] dependencies)
        => ProjectReference(assemblyPath, treatAsUsed: false, dependencies);
    private static ReferenceInfo ProjectReference(string assemblyPath, bool treatAsUsed, params ReferenceInfo[] dependencies)
        => new(ReferenceType.Project,
            itemSpecification: Path.GetFileName(assemblyPath),
            treatAsUsed,
            compilationAssemblies: ImmutableArray.Create(assemblyPath),
            dependencies.ToImmutableArray());
 
    private static ReferenceInfo PackageReference(string assemblyPath, params ReferenceInfo[] dependencies)
        => PackageReference(assemblyPath, treatAsUsed: false, dependencies);
    private static ReferenceInfo PackageReference(string assemblyPath, bool treatAsUsed, params ReferenceInfo[] dependencies)
        => new(ReferenceType.Package,
            itemSpecification: Path.GetFileName(assemblyPath),
            treatAsUsed,
            compilationAssemblies: ImmutableArray.Create(assemblyPath),
            dependencies.ToImmutableArray());
 
    private static ReferenceInfo AssemblyReference(string assemblyPath)
        => AssemblyReference(assemblyPath, treatAsUsed: false);
    private static ReferenceInfo AssemblyReference(string assemblyPath, bool treatAsUsed)
        => new(ReferenceType.Assembly,
            itemSpecification: Path.GetFileName(assemblyPath),
            treatAsUsed,
            compilationAssemblies: ImmutableArray.Create(assemblyPath),
            dependencies: ImmutableArray<ReferenceInfo>.Empty);
 
    private class TestReferenceCleanupService : IReferenceCleanupService
    {
        private readonly List<ReferenceUpdate> _appliedUpdates = [];
        public IReadOnlyList<ReferenceUpdate> AppliedUpdates => _appliedUpdates;
 
        public Task<ImmutableArray<ReferenceInfo>> GetProjectReferencesAsync(string projectPath, CancellationToken cancellationToken)
        {
            throw new System.NotImplementedException();
        }
 
        public Task<bool> TryUpdateReferenceAsync(string projectPath, ReferenceUpdate referenceUpdate, CancellationToken cancellationToken)
        {
            _appliedUpdates.Add(referenceUpdate);
            return Task.FromResult(true);
        }
    }
}