File: GivenThatWeWantToUseAnalyzers.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Build.Tests\Microsoft.NET.Build.Tests.csproj (Microsoft.NET.Build.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
using Microsoft.DotNet.Configurer;
 
namespace Microsoft.NET.Build.Tests
{
    public class GivenThatWeWantToUseAnalyzers : SdkTest
    {
        public GivenThatWeWantToUseAnalyzers(ITestOutputHelper log) : base(log)
        {
        }
 
        [Theory]
        [InlineData("WebApp", false)]
        [InlineData("WebApp", true)]
        [InlineData("WebApp", null)]
        public void It_resolves_requestdelegategenerator_correctly(string testAssetName, bool? isEnabled)
        {
            var asset = _testAssetsManager
                .CopyTestAsset(testAssetName, identifier: isEnabled.ToString())
                .WithSource()
                .WithProjectChanges(project =>
                {
                    if (isEnabled != null)
                    {
                        var ns = project.Root.Name.Namespace;
                        project.Root.Add(new XElement(ns + "PropertyGroup", new XElement("EnableRequestDelegateGenerator", isEnabled)));
                    }
                });
 
            VerifyRequestDelegateGeneratorIsUsed(asset, isEnabled);
            VerifyInterceptorsFeatureProperties(asset, isEnabled, "Microsoft.AspNetCore.Http.Generated");
        }
 
        [Theory]
        [InlineData("WebApp", false)]
        [InlineData("WebApp", true)]
        [InlineData("WebApp", null)]
        public void It_resolves_configbindinggenerator_correctly(string testAssetName, bool? isEnabled)
        {
            var asset = _testAssetsManager
                .CopyTestAsset(testAssetName, identifier: isEnabled.ToString())
                .WithSource()
                .WithProjectChanges(project =>
                {
                    if (isEnabled != null)
                    {
                        var ns = project.Root.Name.Namespace;
                        project.Root.Add(new XElement(ns + "PropertyGroup", new XElement("EnableConfigurationBindingGenerator", isEnabled)));
                    }
                });
 
            VerifyConfigBindingGeneratorIsUsed(asset, isEnabled);
            VerifyInterceptorsFeatureProperties(asset, isEnabled, "Microsoft.Extensions.Configuration.Binder.SourceGeneration");
        }
 
        [Fact]
        public void It_enables_requestdelegategenerator_and_configbindinggenerator_for_PublishAot()
        {
            var asset = _testAssetsManager
                .CopyTestAsset("WebApp")
                .WithSource()
                .WithProjectChanges(project =>
                {
                    var ns = project.Root.Name.Namespace;
                    project.Root.Add(new XElement(ns + "PropertyGroup", new XElement("PublishAot", "true")));
                });
 
            VerifyRequestDelegateGeneratorIsUsed(asset, expectEnabled: true);
            VerifyConfigBindingGeneratorIsUsed(asset, expectEnabled: true);
            VerifyInterceptorsFeatureProperties(asset, expectEnabled: true, "Microsoft.AspNetCore.Http.Generated", "Microsoft.Extensions.Configuration.Binder.SourceGeneration");
        }
 
        [Theory]
        [InlineData("net10.0", true)]
        [InlineData("net9.0", false)]
        [InlineData("net8.0", false)]
        public void It_enables_validationsgenerator_correctly_for_TargetFramework(string targetFramework, bool expectEnabled)
        {
            var asset = _testAssetsManager
                .CopyTestAsset("WebApp")
                .WithSource()
                .WithTargetFramework(targetFramework);
 
            VerifyValidationsGeneratorIsUsed(asset, expectEnabled);
            VerifyInterceptorsFeatureProperties(asset, expectEnabled, "Microsoft.Extensions.Validation.Generated");
        }
 
        [Fact]
        public void It_enables_requestdelegategenerator_and_configbindinggenerator_for_PublishTrimmed()
        {
            var asset = _testAssetsManager
                .CopyTestAsset("WebApp")
                .WithSource()
                .WithProjectChanges(project =>
                {
                    var ns = project.Root.Name.Namespace;
                    project.Root.Add(new XElement(ns + "PropertyGroup", new XElement("PublishTrimmed", "true")));
                });
 
            VerifyRequestDelegateGeneratorIsUsed(asset, expectEnabled: true);
            VerifyConfigBindingGeneratorIsUsed(asset, expectEnabled: true);
            VerifyInterceptorsFeatureProperties(asset, expectEnabled: true, "Microsoft.AspNetCore.Http.Generated", "Microsoft.Extensions.Configuration.Binder.SourceGeneration");
        }
 
        private void VerifyGeneratorIsUsed(TestAsset asset, bool? expectEnabled, string generatorName)
        {
            var command = new GetValuesCommand(
                Log,
                asset.Path,
                ToolsetInfo.CurrentTargetFramework,
                "Analyzer",
                GetValuesCommand.ValueType.Item);
 
            command
                .WithWorkingDirectory(asset.Path)
                .Execute()
                .Should().Pass();
 
            var analyzers = command.GetValues();
 
            Assert.Equal(expectEnabled ?? false, analyzers.Any(analyzer => analyzer.Contains(generatorName)));
        }
 
        private void VerifyRequestDelegateGeneratorIsUsed(TestAsset asset, bool? expectEnabled)
            => VerifyGeneratorIsUsed(asset, expectEnabled, "Microsoft.AspNetCore.Http.RequestDelegateGenerator.dll");
 
        private void VerifyConfigBindingGeneratorIsUsed(TestAsset asset, bool? expectEnabled)
            => VerifyGeneratorIsUsed(asset, expectEnabled, "Microsoft.Extensions.Configuration.Binder.SourceGeneration.dll");
 
        private void VerifyValidationsGeneratorIsUsed(TestAsset asset, bool? expectEnabled)
            => VerifyGeneratorIsUsed(asset, expectEnabled, "Microsoft.Extensions.Validation.ValidationsGenerator.dll");
 
        private void VerifyInterceptorsFeatureProperties(TestAsset asset, bool? expectEnabled, params string[] expectedNamespaces)
        {
            var command = new GetValuesCommand(
                Log,
                asset.Path,
                ToolsetInfo.CurrentTargetFramework,
                "InterceptorsPreviewNamespaces",
                GetValuesCommand.ValueType.Property);
 
            command
                .WithWorkingDirectory(asset.Path)
                .Execute()
                .Should().Pass();
 
            var namespaces = command.GetValues();
 
            Assert.Equal(expectEnabled ?? false, expectedNamespaces.All(expectedNamespace => namespaces.Contains(expectedNamespace)));
        }
 
        [Fact]
        public void It_enables_aspnet_generators_for_non_web_projects_with_framework_reference()
        {
            var testProject = new TestProject()
            {
                Name = "NonWebAppWithAspNet",
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
                IsSdkProject = true,
                IsExe = true,
            };
 
            testProject.AdditionalProperties["ImplicitUsings"] = "Enable";
 
            testProject.ProjectChanges.Add(project =>
                 {
                     var ns = project.Root.Name.Namespace;
 
                     // Add FrameworkReference to ASP.NET Core (this is key to reproducing the issue)
                     project.Root.Add(new XElement(ns + "ItemGroup",
                         new XElement(ns + "FrameworkReference", new XAttribute("Include", "Microsoft.AspNetCore.App"))));
 
                     // Enable configuration binding generator explicitly (like the repro in the issue)
                     project.Root.Add(new XElement(ns + "PropertyGroup",
                         new XElement(ns + "EnableConfigurationBindingGenerator", "true")));
                 });
 
            testProject.SourceFiles["Program.cs"] = """
                using Microsoft.Extensions.Configuration;
 
                var c = new ConfigurationBuilder()
                    .AddInMemoryCollection(new Dictionary<string,string?>()
                    {
                        ["Value"] = "42",
                    })
                    .Build();
                C value = new();
                c.Bind(value);
 
                class C { public int Value { get; set; } }
                """;
 
            // Create a simple non-web project with ASP.NET FrameworkReference 
            var asset = _testAssetsManager
                .CreateTestProject(testProject);
 
            new BuildCommand(asset)
                .Execute()
                .Should().Pass();
 
            // Get the actual values to see what's happening
            var command = new GetValuesCommand(
                Log,
                Path.Combine(asset.Path, "NonWebAppWithAspNet"),
                ToolsetInfo.CurrentTargetFramework,
                "InterceptorsPreviewNamespaces",
                GetValuesCommand.ValueType.Property);
 
            command
                .WithWorkingDirectory(asset.Path)
                .Execute()
                .Should().Pass();
 
            var namespaces = command.GetValues();
            
            // This should work correctly - the non-web project should get the InterceptorsPreviewNamespaces
            // because the logic was moved from Web SDK to FrameworkReferenceResolution targets
            Assert.True(namespaces.Contains("Microsoft.Extensions.Configuration.Binder.SourceGeneration"), 
                $"Expected InterceptorsPreviewNamespaces to contain 'Microsoft.Extensions.Configuration.Binder.SourceGeneration' but got: [{string.Join(", ", namespaces)}]");
        }
 
        [Theory]
        [InlineData("C#", "AppWithLibrary")]
        [InlineData("VB", "AppWithLibraryVB")]
        [InlineData("F#", "AppWithLibraryFS")]
        public void It_resolves_analyzers_correctly(string language, string testAssetName)
        {
            var asset = _testAssetsManager
                .CopyTestAsset(testAssetName, identifier: language)
                .WithSource()
                .WithProjectChanges(project =>
                {
                    var ns = project.Root.Name.Namespace;
                    project.Root.Add(
                        new XElement(ns + "ItemGroup",
                            new XElement(ns + "PackageReference",
                                new XAttribute("Include", "Microsoft.DependencyValidation.Analyzers"),
                                new XAttribute("Version", "0.9.0")),
                            new XElement(ns + "PackageReference",
                                new XAttribute("Include", "Microsoft.CodeQuality.Analyzers"),
                                new XAttribute("Version", "2.6.0"))));
                });
 
            var command = new GetValuesCommand(
                Log,
                Path.Combine(asset.Path, "TestApp"),
                ToolsetInfo.CurrentTargetFramework,
                "Analyzer",
                GetValuesCommand.ValueType.Item);
 
            command
                .WithWorkingDirectory(asset.Path)
                .Execute()
                .Should().Pass();
 
            var analyzers = command.GetValues();
 
            switch (language)
            {
                case "C#":
                    analyzers.Select(x => GetPackageAndPath(x)).Should().BeEquivalentTo(new[]
                            {
                                ("microsoft.net.sdk", (string) null, "analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll"),
                                ("microsoft.net.sdk", (string)null, "analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll"),
                                ("microsoft.netcore.app.ref", (string)null, "analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll"),
                                ("microsoft.netcore.app.ref", (string)null, "analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll"),
                                ("microsoft.codequality.analyzers", "2.6.0", "analyzers/dotnet/cs/Microsoft.CodeQuality.Analyzers.dll"),
                                ("microsoft.codequality.analyzers", "2.6.0", "analyzers/dotnet/cs/Microsoft.CodeQuality.CSharp.Analyzers.dll"),
                                ("microsoft.dependencyvalidation.analyzers", "0.9.0", "analyzers/dotnet/Microsoft.DependencyValidation.Analyzers.dll"),
                                ("microsoft.netcore.app.ref", (string)null, "analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll"),
                                ("microsoft.netcore.app.ref", (string)null, "analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll"),
                                ("microsoft.netcore.app.ref", (string)null, "analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll"),
                                ("microsoft.netcore.app.ref", (string)null, "analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll")
                            }
                        );
                    break;
 
                case "VB":
                    analyzers.Select(x => GetPackageAndPath(x)).Should().BeEquivalentTo(new[]
                        {
                            ("microsoft.net.sdk", (string)null, "analyzers/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers.dll"),
                            ("microsoft.net.sdk", (string)null, "analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll"),
                            ("microsoft.codequality.analyzers", "2.6.0", "analyzers/dotnet/vb/Microsoft.CodeQuality.Analyzers.dll"),
                            ("microsoft.codequality.analyzers", "2.6.0", "analyzers/dotnet/vb/Microsoft.CodeQuality.VisualBasic.Analyzers.dll"),
                            ("microsoft.dependencyvalidation.analyzers", "0.9.0", "analyzers/dotnet/Microsoft.DependencyValidation.Analyzers.dll")
                        }
                        );
                    break;
 
                case "F#":
                    analyzers.Should().BeEmpty();
                    break;
 
                default:
                    throw new ArgumentOutOfRangeException(nameof(language));
            }
        }
 
        [Fact]
        public void It_resolves_multitargeted_analyzers()
        {
            var testProject = new TestProject()
            {
                TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net472"
            };
 
            //  Disable analyzers built in to the SDK so we can more easily test the ones coming from NuGet packages
            testProject.AdditionalProperties["EnableNETAnalyzers"] = "false";
 
            testProject.ProjectChanges.Add(project =>
            {
                var ns = project.Root.Name.Namespace;
 
                var itemGroup = XElement.Parse($@"
  <ItemGroup>
    <PackageReference Include=""System.Text.Json"" Version=""4.7.0"" Condition="" '$(TargetFramework)' == 'net472' "" />
    <PackageReference Include=""System.Text.Json"" Version=""6.0.0-preview.4.21253.7"" Condition="" '$(TargetFramework)' == '{ToolsetInfo.CurrentTargetFramework}' "" />
  </ItemGroup>");
 
                project.Root.Add(itemGroup);
            });
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject);
 
            List<(string package, string version, string path)> GetAnalyzersForTargetFramework(string targetFramework)
            {
                var getValuesCommand = new GetValuesCommand(testAsset,
                    valueName: "Analyzer",
                    GetValuesCommand.ValueType.Item,
                    targetFramework)
                {
                    DependsOnTargets = "ResolveLockFileAnalyzers"
                };
 
                getValuesCommand.Execute("-p:TargetFramework=" + targetFramework).Should().Pass();
 
                return getValuesCommand.GetValues().Select(x => GetPackageAndPath(x)).ToList();
            }
 
            GetAnalyzersForTargetFramework(ToolsetInfo.CurrentTargetFramework).Should().BeEquivalentTo(new[] { ("system.text.json", "6.0.0-preview.4.21253.7", "analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll") });
            GetAnalyzersForTargetFramework("net472").Should().BeEmpty();
        }
 
        static readonly List<string> nugetRoots = new()
            {
                TestContext.Current.NuGetCachePath,
                Path.Combine(CliFolderPathCalculator.DotnetHomePath, ".dotnet", "NuGetFallbackFolder"),
                Path.Combine(TestContext.Current.ToolsetUnderTest.DotNetRoot, "packs")
            };
 
        static (string package, string version, string path) GetPackageAndPath(string absolutePath)
        {
            absolutePath = Path.GetFullPath(absolutePath);
 
            if (absolutePath.StartsWith(TestContext.Current.ToolsetUnderTest.SdksPath))
            {
                string path = absolutePath.Substring(TestContext.Current.ToolsetUnderTest.SdksPath.Length + 1)
                    .Replace(Path.DirectorySeparatorChar, '/');
                var components = path.Split(new char[] { '/' }, 2);
                string sdkName = components[0];
                string pathInSdk = components[1];
                return (sdkName.ToLowerInvariant(), null, pathInSdk);
            }
 
            foreach (var nugetRoot in nugetRoots)
            {
                if (absolutePath.StartsWith(nugetRoot + Path.DirectorySeparatorChar))
                {
                    string path = absolutePath.Substring(nugetRoot.Length + 1)
                        .Replace(Path.DirectorySeparatorChar, '/');
                    var components = path.Split(new char[] { '/' }, 3);
                    var packageName = components[0];
                    var packageVersion = components[1];
                    var pathInPackage = components[2];
                    //  Don't check package version for analyzers included in targeting pack, as the version changes during development
                    if (packageName.Equals("microsoft.netcore.app.ref", StringComparison.OrdinalIgnoreCase))
                    {
                        packageVersion = null;
                    }
                    return (packageName.ToLowerInvariant(), packageVersion, pathInPackage);
                }
            }
 
            throw new InvalidDataException("Expected path to be under a known root: " + absolutePath);
        }
    }
}