Browse Source

Fork component analyzers (#39796)

* Fork component analyzers

This is an attempt to resolve the open https://github.com/dotnet/aspnetcore/pull/30102 PR. Microsoft.AspNetCore.Components.Analyzers ships as a part
of the .NET SDK and the analysis applies to all apps starting in .NET 3.1 or newer. Adding new analyzers to this assembly was problematic since it
would result in new warnings in existing apps when users updated the SDK.

This PR forks the Component.Analyzers to produce a new Components.SDKAnalyzers analyzer assembly. Components.Analyzers is also added to the targeting
pack so it allows us to produce TFM specific analysis.

* Apply suggestions from code review

Co-authored-by: Tanay Parikh <[email protected]>

Co-authored-by: Tanay Parikh <[email protected]>
Pranav K 4 years ago
parent
commit
112a0de144
33 changed files with 3029 additions and 1 deletions
  1. 44 0
      AspNetCore.sln
  2. 6 1
      src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj
  3. 110 0
      src/Tools/SDK-Analyzers/Components/src/ComponentFacts.cs
  4. 54 0
      src/Tools/SDK-Analyzers/Components/src/ComponentInternalUsageDiagnosticAnalzyer.cs
  5. 126 0
      src/Tools/SDK-Analyzers/Components/src/ComponentParameterAnalyzer.cs
  6. 113 0
      src/Tools/SDK-Analyzers/Components/src/ComponentParameterUsageAnalyzer.cs
  7. 88 0
      src/Tools/SDK-Analyzers/Components/src/ComponentParametersShouldBePublicCodeFixProvider.cs
  8. 76 0
      src/Tools/SDK-Analyzers/Components/src/ComponentSymbols.cs
  9. 31 0
      src/Tools/SDK-Analyzers/Components/src/ComponentsApi.cs
  10. 68 0
      src/Tools/SDK-Analyzers/Components/src/DiagnosticDescriptors.cs
  11. 187 0
      src/Tools/SDK-Analyzers/Components/src/InternalUsageAnalyzer.cs
  12. 28 0
      src/Tools/SDK-Analyzers/Components/src/Microsoft.AspNetCore.Components.SdkAnalyzers.csproj
  13. 177 0
      src/Tools/SDK-Analyzers/Components/src/Resources.resx
  14. 46 0
      src/Tools/SDK-Analyzers/Components/test/AnalyzerTestBase.cs
  15. 28 0
      src/Tools/SDK-Analyzers/Components/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs
  16. 102 0
      src/Tools/SDK-Analyzers/Components/test/ComponentInternalUsageDiagnosticsAnalyzerTest.cs
  17. 78 0
      src/Tools/SDK-Analyzers/Components/test/ComponentParameterCaptureUnmatchedValuesHasWrongTypeTest.cs
  18. 66 0
      src/Tools/SDK-Analyzers/Components/test/ComponentParameterCaptureUnmatchedValuesMustBeUniqueTest.cs
  19. 109 0
      src/Tools/SDK-Analyzers/Components/test/ComponentParameterSettersShouldBePublicTest.cs
  20. 359 0
      src/Tools/SDK-Analyzers/Components/test/ComponentParameterUsageAnalyzerTest.cs
  21. 124 0
      src/Tools/SDK-Analyzers/Components/test/ComponentParametersShouldBePublicCodeFixProviderTest.cs
  22. 104 0
      src/Tools/SDK-Analyzers/Components/test/ComponentParametersShouldBePublicTest.cs
  23. 25 0
      src/Tools/SDK-Analyzers/Components/test/ComponentsTestDeclarations.cs
  24. 86 0
      src/Tools/SDK-Analyzers/Components/test/Helpers/CodeFixVerifier.Helper.cs
  25. 90 0
      src/Tools/SDK-Analyzers/Components/test/Helpers/DiagnosticResult.cs
  26. 171 0
      src/Tools/SDK-Analyzers/Components/test/Helpers/DiagnosticVerifier.Helper.cs
  27. 22 0
      src/Tools/SDK-Analyzers/Components/test/Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj
  28. 23 0
      src/Tools/SDK-Analyzers/Components/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsersRendererTypesInMethodBody.cs
  29. 28 0
      src/Tools/SDK-Analyzers/Components/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRendererAsBaseClass.cs
  30. 39 0
      src/Tools/SDK-Analyzers/Components/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRendererTypesInDeclarations.cs
  31. 131 0
      src/Tools/SDK-Analyzers/Components/test/Verifiers/CodeFixVerifier.cs
  32. 287 0
      src/Tools/SDK-Analyzers/Components/test/Verifiers/DiagnosticVerifier.cs
  33. 3 0
      src/Tools/SDK-Analyzers/README.md

+ 44 - 0
AspNetCore.sln

@@ -1650,6 +1650,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.ShadowCopy.Tests", "src\Servers\IIS\IIS\test\IIS.ShadowCopy.Tests\IIS.ShadowCopy.Tests.csproj", "{93D3CC76-9FA9-4198-B49D-54BA918105EE}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK-Analyzers", "SDK-Analyzers", "{6C06163A-80E9-49C1-817C-B391852BA563}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.SdkAnalyzers", "src\Tools\SDK-Analyzers\Components\src\Microsoft.AspNetCore.Components.SdkAnalyzers.csproj", "{825BCF97-67A9-4834-B3A8-C3DC97A90E41}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -9926,6 +9934,38 @@ Global
 		{93D3CC76-9FA9-4198-B49D-54BA918105EE}.Release|x64.Build.0 = Release|Any CPU
 		{93D3CC76-9FA9-4198-B49D-54BA918105EE}.Release|x86.ActiveCfg = Release|Any CPU
 		{93D3CC76-9FA9-4198-B49D-54BA918105EE}.Release|x86.Build.0 = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|arm64.Build.0 = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|x64.Build.0 = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Debug|x86.Build.0 = Debug|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|Any CPU.Build.0 = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|arm64.ActiveCfg = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|arm64.Build.0 = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|x64.ActiveCfg = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|x64.Build.0 = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|x86.ActiveCfg = Release|Any CPU
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41}.Release|x86.Build.0 = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|arm64.Build.0 = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|x64.Build.0 = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Debug|x86.Build.0 = Debug|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|arm64.ActiveCfg = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|arm64.Build.0 = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x64.ActiveCfg = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x64.Build.0 = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.ActiveCfg = Release|Any CPU
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -10742,6 +10782,10 @@ Global
 		{E0BE6B86-F8DB-405D-AC05-78C8C9D3857D} = {C3722C5D-E159-4AB3-AF60-769185B31B47}
 		{8EB0B983-8851-4565-B92F-366F1B126E61} = {C3722C5D-E159-4AB3-AF60-769185B31B47}
 		{93D3CC76-9FA9-4198-B49D-54BA918105EE} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
+		{6C06163A-80E9-49C1-817C-B391852BA563} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
+		{CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} = {6C06163A-80E9-49C1-817C-B391852BA563}
+		{825BCF97-67A9-4834-B3A8-C3DC97A90E41} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
+		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

+ 6 - 1
src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj

@@ -63,11 +63,15 @@ This package is an internal implementation of the .NET Core SDK and is not meant
     <!-- Note: do not add _TransitiveExternalAspNetCoreAppReference to this list. This is intentionally not listed as a direct package reference. -->
     <Reference Include="@(AspNetCoreAppReference);@(AspNetCoreAppReferenceAndPackage);@(ExternalAspNetCoreAppReference)" />
 
-    <!-- Also ensure an Microsoft.AspNetCore.App.Analyzers build. -->
+    <!-- Ensure prerequisite analyzers are built. -->
     <ProjectReference Include="..\..\AspNetCoreAnalyzers\src\CodeFixes\Microsoft.AspNetCore.App.CodeFixes.csproj"
       Private="false"
       ReferenceOutputAssembly="false" />
 
+    <ProjectReference Include="$(RepoRoot)src\Components\Analyzers\src\Microsoft.AspNetCore.Components.Analyzers.csproj"
+      Private="false"
+      ReferenceOutputAssembly="false" />
+
     <!-- Enforce build order. Targeting pack needs to bundle analyzers and information about the runtime. -->
     <ProjectReference Include="..\..\App.Runtime\src\Microsoft.AspNetCore.App.Runtime.csproj"
       Private="false"
@@ -156,6 +160,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant
 
       <RefPackContent Include="$(PkgMicrosoft_Internal_Runtime_AspNetCore_Transport)\$(AnalyzersPackagePath)**\*.*" PackagePath="$(AnalyzersPackagePath)" />
       <RefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
+      <RefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Components.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
       <RefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.CodeFixes\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.CodeFixes.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
 
       <RefPackContent Include="@(AspNetCoreReferenceAssemblyPath)" PackagePath="$(RefAssemblyPackagePath)" />

+ 110 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentFacts.cs

@@ -0,0 +1,110 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+internal static class ComponentFacts
+{
+    public static bool IsAnyParameter(ComponentSymbols symbols, IPropertySymbol property)
+    {
+        if (symbols == null)
+        {
+            throw new ArgumentNullException(nameof(symbols));
+        }
+
+        if (property == null)
+        {
+            throw new ArgumentNullException(nameof(property));
+        }
+
+        return property.GetAttributes().Any(a =>
+        {
+            return SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.ParameterAttribute) || SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.CascadingParameterAttribute);
+        });
+    }
+
+    public static bool IsParameter(ComponentSymbols symbols, IPropertySymbol property)
+    {
+        if (symbols == null)
+        {
+            throw new ArgumentNullException(nameof(symbols));
+        }
+
+        if (property == null)
+        {
+            throw new ArgumentNullException(nameof(property));
+        }
+
+        return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.ParameterAttribute));
+    }
+
+    public static bool IsParameterWithCaptureUnmatchedValues(ComponentSymbols symbols, IPropertySymbol property)
+    {
+        if (symbols == null)
+        {
+            throw new ArgumentNullException(nameof(symbols));
+        }
+
+        if (property == null)
+        {
+            throw new ArgumentNullException(nameof(property));
+        }
+
+        var attribute = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.ParameterAttribute));
+        if (attribute == null)
+        {
+            return false;
+        }
+
+        foreach (var kvp in attribute.NamedArguments)
+        {
+            if (string.Equals(kvp.Key, ComponentsApi.ParameterAttribute.CaptureUnmatchedValues, StringComparison.Ordinal))
+            {
+                return kvp.Value.Value as bool? ?? false;
+            }
+        }
+
+        return false;
+    }
+
+    public static bool IsCascadingParameter(ComponentSymbols symbols, IPropertySymbol property)
+    {
+        if (symbols == null)
+        {
+            throw new ArgumentNullException(nameof(symbols));
+        }
+
+        if (property == null)
+        {
+            throw new ArgumentNullException(nameof(property));
+        }
+
+        return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.CascadingParameterAttribute));
+    }
+
+    public static bool IsComponent(ComponentSymbols symbols, Compilation compilation, INamedTypeSymbol type)
+    {
+        if (symbols is null)
+        {
+            throw new ArgumentNullException(nameof(symbols));
+        }
+
+        if (type is null)
+        {
+            throw new ArgumentNullException(nameof(type));
+        }
+
+        var conversion = compilation.ClassifyConversion(symbols.IComponentType, type);
+        if (!conversion.Exists || !conversion.IsExplicit)
+        {
+            return false;
+        }
+
+        return true;
+    }
+}

+ 54 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentInternalUsageDiagnosticAnalzyer.cs

@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Immutable;
+using Microsoft.AspNetCore.Components.Analyzers;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Microsoft.Extensions.Internal;
+
+/// <summary>
+/// This API supports infrastructure and is not intended to be used
+/// directly from your code. This API may change or be removed in future releases.
+/// </summary>
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class ComponentInternalUsageDiagnosticAnalyzer : DiagnosticAnalyzer
+{
+    private static readonly string[] NamespaceParts = new[] { "RenderTree", "Components", "AspNetCore", "Microsoft", };
+
+    private readonly InternalUsageAnalyzer _inner;
+
+    public ComponentInternalUsageDiagnosticAnalyzer()
+    {
+        // We don't have in *internal* attribute in Blazor.
+        _inner = new InternalUsageAnalyzer(IsInInternalNamespace, hasInternalAttribute: null, DiagnosticDescriptors.DoNotUseRenderTreeTypes);
+    }
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DoNotUseRenderTreeTypes);
+
+    public override void Initialize(AnalysisContext context)
+    {
+        context.EnableConcurrentExecution();
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+
+        _inner.Register(context);
+    }
+
+    private static bool IsInInternalNamespace(ISymbol symbol)
+    {
+        var @namespace = symbol?.ContainingNamespace;
+        for (var i = 0; i < NamespaceParts.Length; i++)
+        {
+            if (@namespace == null || !string.Equals(NamespaceParts[i], @namespace.Name, StringComparison.Ordinal))
+            {
+                return false;
+            }
+
+            @namespace = @namespace.ContainingNamespace;
+        }
+
+        return @namespace.IsGlobalNamespace;
+    }
+}

+ 126 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentParameterAnalyzer.cs

@@ -0,0 +1,126 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class ComponentParameterAnalyzer : DiagnosticAnalyzer
+{
+    public ComponentParameterAnalyzer()
+    {
+        SupportedDiagnostics = ImmutableArray.Create(new[]
+        {
+            DiagnosticDescriptors.ComponentParametersShouldBePublic,
+            DiagnosticDescriptors.ComponentParameterSettersShouldBePublic,
+            DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
+            DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
+        });
+    }
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
+
+    public override void Initialize(AnalysisContext context)
+    {
+        context.EnableConcurrentExecution();
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+        context.RegisterCompilationStartAction(context =>
+        {
+            if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
+            {
+                // Types we need are not defined.
+                return;
+            }
+
+            // This operates per-type because one of the validations we need has to look for duplicates
+            // defined on the same type.
+            context.RegisterSymbolStartAction(context =>
+            {
+                var properties = new List<IPropertySymbol>();
+
+                var type = (INamedTypeSymbol)context.Symbol;
+                foreach (var member in type.GetMembers())
+                {
+                    if (member is IPropertySymbol property && ComponentFacts.IsParameter(symbols, property))
+                    {
+                        // Annotated with [Parameter]. We ignore [CascadingParameter]'s because they don't interact with tooling and don't currently have any analyzer restrictions.
+                        properties.Add(property);
+                    }
+                }
+
+                if (properties.Count == 0)
+                {
+                    return;
+                }
+
+                context.RegisterSymbolEndAction(context =>
+                {
+                    var captureUnmatchedValuesParameters = new List<IPropertySymbol>();
+
+                    // Per-property validations
+                    foreach (var property in properties)
+                    {
+                        var propertyLocation = property.Locations.FirstOrDefault();
+                        if (propertyLocation == null)
+                        {
+                            continue;
+                        }
+
+                        if (property.DeclaredAccessibility != Accessibility.Public)
+                        {
+                            context.ReportDiagnostic(Diagnostic.Create(
+                                DiagnosticDescriptors.ComponentParametersShouldBePublic,
+                                propertyLocation,
+                                property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
+                        }
+                        else if (property.SetMethod?.DeclaredAccessibility != Accessibility.Public)
+                        {
+                            context.ReportDiagnostic(Diagnostic.Create(
+                                DiagnosticDescriptors.ComponentParameterSettersShouldBePublic,
+                                propertyLocation,
+                                property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
+                        }
+
+                        if (ComponentFacts.IsParameterWithCaptureUnmatchedValues(symbols, property))
+                        {
+                            captureUnmatchedValuesParameters.Add(property);
+
+                            // Check the type, we need to be able to assign a Dictionary<string, object>
+                            var conversion = context.Compilation.ClassifyConversion(symbols.ParameterCaptureUnmatchedValuesRuntimeType, property.Type);
+                            if (!conversion.Exists || conversion.IsExplicit)
+                            {
+                                context.ReportDiagnostic(Diagnostic.Create(
+                                    DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
+                                    propertyLocation,
+                                    property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
+                                    property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
+                                    symbols.ParameterCaptureUnmatchedValuesRuntimeType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
+                            }
+                        }
+                    }
+
+                    // Check if the type defines multiple CaptureUnmatchedValues parameters. Doing this outside the loop means we place the
+                    // errors on the type.
+                    if (captureUnmatchedValuesParameters.Count > 1)
+                    {
+                        context.ReportDiagnostic(Diagnostic.Create(
+                            DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
+                            context.Symbol.Locations[0],
+                            type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
+                            Environment.NewLine,
+                            string.Join(
+                                Environment.NewLine,
+                                captureUnmatchedValuesParameters.Select(p => p.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)).OrderBy(n => n))));
+                    }
+                });
+            }, SymbolKind.NamedType);
+        });
+    }
+}

+ 113 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentParameterUsageAnalyzer.cs

@@ -0,0 +1,113 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class ComponentParameterUsageAnalyzer : DiagnosticAnalyzer
+{
+    public ComponentParameterUsageAnalyzer()
+    {
+        SupportedDiagnostics = ImmutableArray.Create(new[]
+        {
+                DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent,
+            });
+    }
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
+
+    public override void Initialize(AnalysisContext context)
+    {
+        context.EnableConcurrentExecution();
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+        context.RegisterCompilationStartAction(context =>
+        {
+            if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
+            {
+                // Types we need are not defined.
+                return;
+            }
+
+            context.RegisterOperationBlockStartAction(startBlockContext =>
+            {
+                startBlockContext.RegisterOperationAction(context =>
+                {
+                    IOperation leftHandSide;
+
+                    if (context.Operation is IAssignmentOperation assignmentOperation)
+                    {
+                        leftHandSide = assignmentOperation.Target;
+                    }
+                    else
+                    {
+                        var incrementOrDecrementOperation = (IIncrementOrDecrementOperation)context.Operation;
+                        leftHandSide = incrementOrDecrementOperation.Target;
+                    }
+
+                    if (leftHandSide == null)
+                    {
+                        // Malformed assignment, no left hand side.
+                        return;
+                    }
+
+                    if (leftHandSide.Kind != OperationKind.PropertyReference)
+                    {
+                        // We don't want to capture situations where a user does
+                        // MyOtherProperty = aComponent.SomeParameter
+                        return;
+                    }
+
+                    var propertyReference = (IPropertyReferenceOperation)leftHandSide;
+                    var componentProperty = (IPropertySymbol)propertyReference.Member;
+
+                    if (!ComponentFacts.IsParameter(symbols, componentProperty))
+                    {
+                        // This is not a property reference that we care about, it is not decorated with [Parameter].
+                        return;
+                    }
+
+                    var propertyContainingType = componentProperty.ContainingType;
+                    if (!ComponentFacts.IsComponent(symbols, context.Compilation, propertyContainingType))
+                    {
+                        // Someone referenced a property as [Parameter] inside something that is not a component.
+                        return;
+                    }
+
+                    var assignmentContainingType = startBlockContext.OwningSymbol?.ContainingType;
+                    if (assignmentContainingType == null)
+                    {
+                        // Assignment location has no containing type. Most likely we're operating on malformed code, don't try and validate.
+                        return;
+                    }
+
+                    var conversion = context.Compilation.ClassifyConversion(propertyContainingType, assignmentContainingType);
+                    if (conversion.Exists && conversion.IsIdentity)
+                    {
+                        // The assignment is taking place inside of the declaring component.
+                        return;
+                    }
+
+                    if (conversion.Exists && conversion.IsExplicit)
+                    {
+                        // The assignment is taking place within the components type hierarchy. This means the user is setting this in a supported
+                        // scenario.
+                        return;
+                    }
+
+                    // At this point the user is referencing a component parameter outside of its declaring class.
+
+                    context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent,
+                    propertyReference.Syntax.GetLocation(),
+                    propertyReference.Member.Name));
+                }, OperationKind.SimpleAssignment, OperationKind.CompoundAssignment, OperationKind.CoalesceAssignment, OperationKind.Increment, OperationKind.Decrement);
+            });
+        });
+    }
+}

+ 88 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentParametersShouldBePublicCodeFixProvider.cs

@@ -0,0 +1,88 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ComponentParametersShouldBePublicCodeFixProvider)), Shared]
+public class ComponentParametersShouldBePublicCodeFixProvider : CodeFixProvider
+{
+    private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldBePublic_FixTitle), Resources.ResourceManager, typeof(Resources));
+
+    public override ImmutableArray<string> FixableDiagnosticIds
+        => ImmutableArray.Create(DiagnosticDescriptors.ComponentParametersShouldBePublic.Id);
+
+    public sealed override FixAllProvider GetFixAllProvider()
+    {
+        // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
+        return WellKnownFixAllProviders.BatchFixer;
+    }
+
+    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+    {
+        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+        var diagnostic = context.Diagnostics.First();
+        var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+        // Find the type declaration identified by the diagnostic.
+        var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
+
+        // Register a code action that will invoke the fix.
+        var title = Title.ToString(CultureInfo.InvariantCulture);
+        context.RegisterCodeFix(
+            CodeAction.Create(
+                title: title,
+                createChangedDocument: c => GetTransformedDocumentAsync(context.Document, root, declaration),
+                equivalenceKey: title),
+            diagnostic);
+    }
+
+    private static Task<Document> GetTransformedDocumentAsync(
+        Document document,
+        SyntaxNode root,
+        PropertyDeclarationSyntax declarationNode)
+    {
+        var updatedDeclarationNode = HandlePropertyDeclaration(declarationNode);
+        var newSyntaxRoot = root.ReplaceNode(declarationNode, updatedDeclarationNode);
+        return Task.FromResult(document.WithSyntaxRoot(newSyntaxRoot));
+    }
+
+    private static SyntaxNode HandlePropertyDeclaration(PropertyDeclarationSyntax node)
+    {
+        TypeSyntax type = node.Type;
+        if (type == null || type.IsMissing)
+        {
+            return null;
+        }
+
+        var newModifiers = node.Modifiers;
+        for (var i = 0; i < node.Modifiers.Count; i++)
+        {
+            var modifier = node.Modifiers[i];
+            if (modifier.IsKind(SyntaxKind.PrivateKeyword) ||
+                modifier.IsKind(SyntaxKind.ProtectedKeyword) ||
+                modifier.IsKind(SyntaxKind.InternalKeyword) ||
+
+                // We also remove public in case the user has written something totally backwards such as private public protected Foo
+                modifier.IsKind(SyntaxKind.PublicKeyword))
+            {
+                newModifiers = newModifiers.Remove(modifier);
+            }
+        }
+
+        var publicModifier = SyntaxFactory.Token(SyntaxKind.PublicKeyword);
+        newModifiers = newModifiers.Insert(0, publicModifier);
+        node = node.WithModifiers(newModifiers);
+        return node;
+    }
+}

+ 76 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentSymbols.cs

@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+internal class ComponentSymbols
+{
+    public static bool TryCreate(Compilation compilation, out ComponentSymbols symbols)
+    {
+        if (compilation == null)
+        {
+            throw new ArgumentNullException(nameof(compilation));
+        }
+
+        var parameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.ParameterAttribute.MetadataName);
+        if (parameterAttribute == null)
+        {
+            symbols = null;
+            return false;
+        }
+
+        var cascadingParameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.CascadingParameterAttribute.MetadataName);
+        if (cascadingParameterAttribute == null)
+        {
+            symbols = null;
+            return false;
+        }
+
+        var icomponentType = compilation.GetTypeByMetadataName(ComponentsApi.IComponent.MetadataName);
+        if (icomponentType == null)
+        {
+            symbols = null;
+            return false;
+        }
+
+        var dictionary = compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");
+        var @string = compilation.GetSpecialType(SpecialType.System_String);
+        var @object = compilation.GetSpecialType(SpecialType.System_Object);
+        if (dictionary == null || @string == null || @object == null)
+        {
+            symbols = null;
+            return false;
+        }
+
+        var parameterCaptureUnmatchedValuesRuntimeType = dictionary.Construct(@string, @object);
+
+        symbols = new ComponentSymbols(
+            parameterAttribute,
+            cascadingParameterAttribute,
+            parameterCaptureUnmatchedValuesRuntimeType,
+            icomponentType);
+        return true;
+    }
+
+    private ComponentSymbols(
+        INamedTypeSymbol parameterAttribute,
+        INamedTypeSymbol cascadingParameterAttribute,
+        INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
+        INamedTypeSymbol icomponentType)
+    {
+        ParameterAttribute = parameterAttribute;
+        CascadingParameterAttribute = cascadingParameterAttribute;
+        ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
+        IComponentType = icomponentType;
+    }
+
+    public INamedTypeSymbol ParameterAttribute { get; }
+    public INamedTypeSymbol ParameterCaptureUnmatchedValuesRuntimeType { get; }
+
+    public INamedTypeSymbol CascadingParameterAttribute { get; }
+
+    public INamedTypeSymbol IComponentType { get; }
+}

+ 31 - 0
src/Tools/SDK-Analyzers/Components/src/ComponentsApi.cs

@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+// Constants for type and method names used in code-generation
+// Keep these in sync with the actual definitions
+internal static class ComponentsApi
+{
+    public const string AssemblyName = "Microsoft.AspNetCore.Components";
+
+    public static class ParameterAttribute
+    {
+        public const string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute";
+        public const string MetadataName = FullTypeName;
+
+        public const string CaptureUnmatchedValues = "CaptureUnmatchedValues";
+    }
+
+    public static class CascadingParameterAttribute
+    {
+        public const string FullTypeName = "Microsoft.AspNetCore.Components.CascadingParameterAttribute";
+        public const string MetadataName = FullTypeName;
+    }
+
+    public static class IComponent
+    {
+        public const string FullTypeName = "Microsoft.AspNetCore.Components.IComponent";
+        public const string MetadataName = FullTypeName;
+    }
+}

+ 68 - 0
src/Tools/SDK-Analyzers/Components/src/DiagnosticDescriptors.cs

@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+[System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
+internal static class DiagnosticDescriptors
+{
+    // Note: The Razor Compiler (including Components features) use the RZ prefix for diagnostics, so there's currently
+    // no change of clashing between that and the BL prefix used here.
+    //
+    // Tracking https://github.com/dotnet/aspnetcore/issues/10382 to rationalize this
+    public static readonly DiagnosticDescriptor ComponentParameterSettersShouldBePublic = new DiagnosticDescriptor(
+        "BL0001",
+        new LocalizableResourceString(nameof(Resources.ComponentParameterSettersShouldBePublic_Title), Resources.ResourceManager, typeof(Resources)),
+        new LocalizableResourceString(nameof(Resources.ComponentParameterSettersShouldBePublic_Format), Resources.ResourceManager, typeof(Resources)),
+        "Encapsulation",
+        DiagnosticSeverity.Error,
+        isEnabledByDefault: true,
+        description: new LocalizableResourceString(nameof(Resources.ComponentParameterSettersShouldBePublic_Description), Resources.ResourceManager, typeof(Resources)));
+
+    public static readonly DiagnosticDescriptor ComponentParameterCaptureUnmatchedValuesMustBeUnique = new DiagnosticDescriptor(
+        "BL0002",
+        new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesMustBeUnique_Title), Resources.ResourceManager, typeof(Resources)),
+        new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesMustBeUnique_Format), Resources.ResourceManager, typeof(Resources)),
+        "Usage",
+        DiagnosticSeverity.Warning,
+        isEnabledByDefault: true,
+        description: new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesMustBeUnique_Description), Resources.ResourceManager, typeof(Resources)));
+
+    public static readonly DiagnosticDescriptor ComponentParameterCaptureUnmatchedValuesHasWrongType = new DiagnosticDescriptor(
+        "BL0003",
+        new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesHasWrongType_Title), Resources.ResourceManager, typeof(Resources)),
+        new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesHasWrongType_Format), Resources.ResourceManager, typeof(Resources)),
+        "Usage",
+        DiagnosticSeverity.Warning,
+        isEnabledByDefault: true,
+        description: new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesHasWrongType_Description), Resources.ResourceManager, typeof(Resources)));
+
+    public static readonly DiagnosticDescriptor ComponentParametersShouldBePublic = new DiagnosticDescriptor(
+        "BL0004",
+        new LocalizableResourceString(nameof(Resources.ComponentParameterShouldBePublic_Title), Resources.ResourceManager, typeof(Resources)),
+        new LocalizableResourceString(nameof(Resources.ComponentParameterShouldBePublic_Format), Resources.ResourceManager, typeof(Resources)),
+        "Encapsulation",
+        DiagnosticSeverity.Error,
+        isEnabledByDefault: true,
+        description: new LocalizableResourceString(nameof(Resources.ComponentParametersShouldBePublic_Description), Resources.ResourceManager, typeof(Resources)));
+
+    public static readonly DiagnosticDescriptor ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent = new DiagnosticDescriptor(
+        "BL0005",
+        new LocalizableResourceString(nameof(Resources.ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Title), Resources.ResourceManager, typeof(Resources)),
+        new LocalizableResourceString(nameof(Resources.ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Format), Resources.ResourceManager, typeof(Resources)),
+        "Usage",
+        DiagnosticSeverity.Warning,
+        isEnabledByDefault: true,
+        description: new LocalizableResourceString(nameof(Resources.ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Description), Resources.ResourceManager, typeof(Resources)));
+
+    public static readonly DiagnosticDescriptor DoNotUseRenderTreeTypes = new DiagnosticDescriptor(
+        "BL0006",
+        new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Title), Resources.ResourceManager, typeof(Resources)),
+        new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Description), Resources.ResourceManager, typeof(Resources)),
+        "Usage",
+        DiagnosticSeverity.Warning,
+        isEnabledByDefault: true,
+        description: new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Description), Resources.ResourceManager, typeof(Resources)));
+}

+ 187 - 0
src/Tools/SDK-Analyzers/Components/src/InternalUsageAnalyzer.cs

@@ -0,0 +1,187 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Microsoft.Extensions.Internal;
+
+internal class InternalUsageAnalyzer
+{
+    private readonly Func<ISymbol, bool> _isInternalNamespace;
+    private readonly Func<ISymbol, bool> _hasInternalAttribute;
+    private readonly DiagnosticDescriptor _descriptor;
+
+    /// <summary>
+    /// Creates a new instance of <see cref="InternalUsageAnalyzer" />. The creator should provide delegates to help determine whether
+    /// a given symbol is internal or not, and a <see cref="DiagnosticDescriptor" /> to create errors.
+    /// </summary>
+    /// <param name="isInInternalNamespace">The delegate used to check if a symbol belongs to an internal namespace.</param>
+    /// <param name="hasInternalAttribute">The delegate used to check if a symbol has an internal attribute.</param>
+    /// <param name="descriptor">
+    /// The <see cref="DiagnosticDescriptor" /> used to create errors. The error message should expect a single parameter
+    /// used for the display name of the member.
+    /// </param>
+    public InternalUsageAnalyzer(Func<ISymbol, bool> isInInternalNamespace, Func<ISymbol, bool> hasInternalAttribute, DiagnosticDescriptor descriptor)
+    {
+        _isInternalNamespace = isInInternalNamespace ?? new Func<ISymbol, bool>((_) => false);
+        _hasInternalAttribute = hasInternalAttribute ?? new Func<ISymbol, bool>((_) => false);
+        _descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
+    }
+
+    public void Register(AnalysisContext context)
+    {
+        context.EnableConcurrentExecution();
+
+        // Analyze usage of our internal types in method bodies.
+        context.RegisterOperationAction(
+            AnalyzeOperation,
+            OperationKind.ObjectCreation,
+            OperationKind.Invocation,
+            OperationKind.FieldReference,
+            OperationKind.MethodReference,
+            OperationKind.PropertyReference,
+            OperationKind.EventReference);
+
+        // Analyze declarations that use our internal types in API surface.
+        context.RegisterSymbolAction(
+            AnalyzeSymbol,
+            SymbolKind.NamedType,
+            SymbolKind.Field,
+            SymbolKind.Method,
+            SymbolKind.Property,
+            SymbolKind.Event);
+    }
+
+    private void AnalyzeOperation(OperationAnalysisContext context)
+    {
+        var symbol = context.Operation switch
+        {
+            IObjectCreationOperation creation => creation.Constructor,
+            IInvocationOperation invocation => invocation.TargetMethod,
+            IFieldReferenceOperation field => field.Member,
+            IMethodReferenceOperation method => method.Member,
+            IPropertyReferenceOperation property => property.Member,
+            IEventReferenceOperation @event => @event.Member,
+            _ => throw new InvalidOperationException("Unexpected operation kind: " + context.Operation.Kind),
+        };
+
+        VisitOperationSymbol(context, symbol);
+    }
+
+    private void AnalyzeSymbol(SymbolAnalysisContext context)
+    {
+        // Note: we don't currently try to detect second-order usage of these types
+        // like public Task<InternalFoo> GetFooAsync() { }.
+        //
+        // This probably accomplishes our goals OK for now, which are focused on use of these
+        // types in method bodies.
+        switch (context.Symbol)
+        {
+            case INamedTypeSymbol type:
+                VisitDeclarationSymbol(context, type.BaseType, type);
+                foreach (var @interface in type.Interfaces)
+                {
+                    VisitDeclarationSymbol(context, @interface, type);
+                }
+                break;
+
+            case IFieldSymbol field:
+                VisitDeclarationSymbol(context, field.Type, field);
+                break;
+
+            case IMethodSymbol method:
+
+                // Ignore return types on property-getters. Those will be reported through
+                // the property analysis.
+                if (method.MethodKind != MethodKind.PropertyGet)
+                {
+                    VisitDeclarationSymbol(context, method.ReturnType, method);
+                }
+
+                // Ignore parameters on property-setters. Those will be reported through
+                // the property analysis.
+                if (method.MethodKind != MethodKind.PropertySet)
+                {
+                    foreach (var parameter in method.Parameters)
+                    {
+                        VisitDeclarationSymbol(context, parameter.Type, method);
+                    }
+                }
+                break;
+
+            case IPropertySymbol property:
+                VisitDeclarationSymbol(context, property.Type, property);
+                break;
+
+            case IEventSymbol @event:
+                VisitDeclarationSymbol(context, @event.Type, @event);
+                break;
+        }
+    }
+
+    // Similar logic here to VisitDeclarationSymbol, keep these in sync.
+    private void VisitOperationSymbol(OperationAnalysisContext context, ISymbol symbol)
+    {
+        if (symbol == null || SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, context.Compilation.Assembly))
+        {
+            // The type is being referenced within the same assembly. This is valid use of an "internal" type
+            return;
+        }
+
+        if (HasInternalAttribute(symbol))
+        {
+            context.ReportDiagnostic(Diagnostic.Create(
+                _descriptor,
+                context.Operation.Syntax.GetLocation(),
+                symbol.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)));
+            return;
+        }
+
+        var containingType = symbol.ContainingType;
+        if (IsInInternalNamespace(containingType) || HasInternalAttribute(containingType))
+        {
+            context.ReportDiagnostic(Diagnostic.Create(
+                _descriptor,
+                context.Operation.Syntax.GetLocation(),
+                containingType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)));
+            return;
+        }
+    }
+
+    // Similar logic here to VisitOperationSymbol, keep these in sync.
+    private void VisitDeclarationSymbol(SymbolAnalysisContext context, ISymbol symbol, ISymbol symbolForDiagnostic)
+    {
+        if (symbol == null || SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, context.Compilation.Assembly))
+        {
+            // This is part of the compilation, avoid this analyzer when building from source.
+            return;
+        }
+
+        if (HasInternalAttribute(symbol))
+        {
+            context.ReportDiagnostic(Diagnostic.Create(
+                _descriptor,
+                symbolForDiagnostic.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation() ?? Location.None,
+                symbol.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)));
+            return;
+        }
+
+        var containingType = symbol as INamedTypeSymbol ?? symbol.ContainingType;
+        if (IsInInternalNamespace(containingType) || HasInternalAttribute(containingType))
+        {
+            context.ReportDiagnostic(Diagnostic.Create(
+                _descriptor,
+                symbolForDiagnostic.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation() ?? Location.None,
+                containingType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)));
+            return;
+        }
+    }
+
+    private bool HasInternalAttribute(ISymbol symbol) => _hasInternalAttribute(symbol);
+
+    private bool IsInInternalNamespace(ISymbol symbol) => _isInternalNamespace(symbol);
+}

+ 28 - 0
src/Tools/SDK-Analyzers/Components/src/Microsoft.AspNetCore.Components.SdkAnalyzers.csproj

@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <IncludeBuildOutput>false</IncludeBuildOutput>
+    <NoPackageAnalysis>true</NoPackageAnalysis>
+    <GenerateDocumentationFile>false</GenerateDocumentationFile>
+    <Description>Roslyn analyzers for ASP.NET Core Components.</Description>
+    <RootNamespace>Microsoft.AspNetCore.Components.Analyzers</RootNamespace>
+    <!-- This package is for internal use only. It contains a CLI which is bundled in the .NET Core SDK. -->
+    <IsShippingPackage>false</IsShippingPackage>
+    <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
+    <IsProjectReferenceProvider>false</IsProjectReferenceProvider>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <!-- This analyzer is supported in VS 2019 and must use a compatible Microsoft.CodeAnalysis version -->
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="All" IsImplicitlyDefined="true" Version="$(Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion)" />
+    <PackageReference Update="NETStandard.Library" PrivateAssets="all" />
+
+    <InternalsVisibleTo Include="Microsoft.AspNetCore.Components.SdkAnalyzers.Tests" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="$(TargetPath)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+  </ItemGroup>
+
+</Project>

+ 177 - 0
src/Tools/SDK-Analyzers/Components/src/Resources.resx

@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!--
+    Microsoft ResX Schema
+
+    Version 2.0
+
+    The primary goals of this format is to allow a simple XML format
+    that is mostly human readable. The generation and parsing of the
+    various data types are done through the TypeConverter classes
+    associated with the data types.
+
+    Example:
+
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+
+    There are any number of "resheader" rows that contain simple
+    name/value pairs.
+
+    Each data row contains a name, and value. The row also contains a
+    type or mimetype. Type corresponds to a .NET class that support
+    text/value conversion through the TypeConverter architecture.
+    Classes that don't support this are serialized and stored with the
+    mimetype set.
+
+    The mimetype is used for serialized objects, and tells the
+    ResXResourceReader how to depersist the object. This is currently not
+    extensible. For a given mimetype the value must be set accordingly:
+
+    Note - application/x-microsoft.net.object.binary.base64 is the format
+    that the ResXResourceWriter will generate, however the reader can
+    read any of the formats listed below.
+
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="ComponentParameterSettersShouldBePublic_Description" xml:space="preserve">
+    <value>Component parameters should have public setters.</value>
+  </data>
+  <data name="ComponentParameterSettersShouldBePublic_Format" xml:space="preserve">
+    <value>Component parameter '{0}' should have a public setter.</value>
+  </data>
+  <data name="ComponentParameterSettersShouldBePublic_Title" xml:space="preserve">
+    <value>Component parameter should have public setters.</value>
+  </data>
+  <data name="ComponentParameterCaptureUnmatchedValuesMustBeUnique_Description" xml:space="preserve">
+    <value>Components may only define a single parameter with CaptureUnmatchedValues.</value>
+  </data>
+  <data name="ComponentParameterCaptureUnmatchedValuesMustBeUnique_Format" xml:space="preserve">
+    <value>Component type '{0}' defines properties multiple parameters with CaptureUnmatchedValues. Properties: {1}{2}</value>
+  </data>
+  <data name="ComponentParameterCaptureUnmatchedValuesMustBeUnique_Title" xml:space="preserve">
+    <value>Component has multiple CaptureUnmatchedValues parameters</value>
+  </data>
+  <data name="ComponentParameterCaptureUnmatchedValuesHasWrongType_Description" xml:space="preserve">
+    <value>Component parameters with CaptureUnmatchedValues must be a correct type.</value>
+  </data>
+  <data name="ComponentParameterCaptureUnmatchedValuesHasWrongType_Format" xml:space="preserve">
+    <value>Component parameter '{0}' defines CaptureUnmatchedValues but has an unsupported type '{1}'. Use a type assignable from '{2}'.</value>
+  </data>
+  <data name="ComponentParameterCaptureUnmatchedValuesHasWrongType_Title" xml:space="preserve">
+    <value>Component parameter with CaptureUnmatchedValues has the wrong type</value>
+  </data>
+  <data name="ComponentParameterShouldBePublic_Format" xml:space="preserve">
+    <value>Component parameter '{0}' should be public.</value>
+  </data>
+  <data name="ComponentParameterShouldBePublic_Title" xml:space="preserve">
+    <value>Component parameter should be public.</value>
+  </data>
+  <data name="ComponentParametersShouldBePublic_Description" xml:space="preserve">
+    <value>Component parameters should be public.</value>
+  </data>
+  <data name="ComponentParametersShouldBePublic_FixTitle" xml:space="preserve">
+    <value>Make component parameters public.</value>
+  </data>
+  <data name="ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Description" xml:space="preserve">
+    <value>Component parameters should not be set outside of their declared component. Doing so may result in unexpected behavior at runtime.</value>
+  </data>
+  <data name="ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Format" xml:space="preserve">
+    <value>Component parameter '{0}' should not be set outside of its component.</value>
+  </data>
+  <data name="ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Title" xml:space="preserve">
+    <value>Component parameter should not be set outside of its component.</value>
+  </data>
+  <data name="DoNotUseRenderTreeTypes_Description" xml:space="preserve">
+    <value>The types in 'Microsoft.AspNetCore.Components.RenderTree' are not recommended for use outside of the Blazor framework. These  type definitions will change in future releases.</value>
+  </data>
+  <data name="DoNotUseRenderTreeTypes_Format" xml:space="preserve">
+    <value>The type or member {0} is is not recommended for use outside of the Blazor frameworks. Types defined in 'Microsoft.AspNetCore.Components.RenderTree' will change in future releases.</value>
+  </data>
+  <data name="DoNotUseRenderTreeTypes_Title" xml:space="preserve">
+    <value>Do not use RenderTree types</value>
+  </data>
+</root>

+ 46 - 0
src/Tools/SDK-Analyzers/Components/test/AnalyzerTestBase.cs

@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Analyzer.Testing;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+public abstract class AnalyzerTestBase
+{
+    // Test files are copied to both the bin/ and publish/ folders. Use BaseDirectory on or off Helix.
+    private static readonly string ProjectDirectory = AppContext.BaseDirectory;
+
+    public TestSource Read(string source)
+    {
+        if (!source.EndsWith(".cs", StringComparison.Ordinal))
+        {
+            source += ".cs";
+        }
+
+        var filePath = Path.Combine(ProjectDirectory, "TestFiles", GetType().Name, source);
+        if (!File.Exists(filePath))
+        {
+            throw new FileNotFoundException($"TestFile {source} could not be found at {filePath}.", filePath);
+        }
+
+        var fileContent = File.ReadAllText(filePath);
+        return TestSource.Read(fileContent);
+    }
+
+    public Project CreateProject(string source)
+    {
+        if (!source.EndsWith(".cs", StringComparison.Ordinal))
+        {
+            source += ".cs";
+        }
+
+        var read = Read(source);
+        return DiagnosticProject.Create(GetType().Assembly, new[] { read.Source, });
+    }
+
+    public Task<Compilation> CreateCompilationAsync(string source)
+    {
+        return CreateProject(source).GetCompilationAsync();
+    }
+}

+ 28 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs

@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Analyzer.Testing;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+internal class ComponentAnalyzerDiagnosticAnalyzerRunner : DiagnosticAnalyzerRunner
+{
+    public ComponentAnalyzerDiagnosticAnalyzerRunner(DiagnosticAnalyzer analyzer)
+    {
+        Analyzer = analyzer;
+    }
+
+    public DiagnosticAnalyzer Analyzer { get; }
+
+    public Task<Diagnostic[]> GetDiagnosticsAsync(string source)
+    {
+        return GetDiagnosticsAsync(sources: new[] { source }, Analyzer, Array.Empty<string>());
+    }
+
+    public Task<Diagnostic[]> GetDiagnosticsAsync(Project project)
+    {
+        return GetDiagnosticsAsync(new[] { project }, Analyzer, Array.Empty<string>());
+    }
+}

+ 102 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentInternalUsageDiagnosticsAnalyzerTest.cs

@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Analyzer.Testing;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+public class ComponentInternalUsageDiagnosticsAnalyzerTest : AnalyzerTestBase
+{
+    public ComponentInternalUsageDiagnosticsAnalyzerTest()
+    {
+        Analyzer = new ComponentInternalUsageDiagnosticAnalyzer();
+        Runner = new ComponentAnalyzerDiagnosticAnalyzerRunner(Analyzer);
+    }
+
+    private ComponentInternalUsageDiagnosticAnalyzer Analyzer { get; }
+    private ComponentAnalyzerDiagnosticAnalyzerRunner Runner { get; }
+
+    [Fact]
+    public async Task InternalUsage_FindsUseOfInternalTypesInDeclarations()
+    {
+        // Arrange
+        var source = Read("UsesRendererTypesInDeclarations");
+
+        // Act
+        var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
+
+        // Assert
+        Assert.Collection(
+            diagnostics,
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMBaseClass"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMField"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMInvocation"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMProperty"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMParameter"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMReturnType"], diagnostic.Location);
+            });
+    }
+
+    [Fact]
+    public async Task InternalUsage_FindsUseOfInternalTypesInMethodBody()
+    {
+        // Arrange
+        var source = Read("UsersRendererTypesInMethodBody");
+
+        // Act
+        var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
+
+        // Assert
+        Assert.Collection(
+            diagnostics,
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMField"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMNewObject"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMProperty"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMNewObject2"], diagnostic.Location);
+            },
+            diagnostic =>
+            {
+                Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor);
+                AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MMInvocation"], diagnostic.Location);
+            });
+    }
+}

+ 78 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentParameterCaptureUnmatchedValuesHasWrongTypeTest.cs

@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Test;
+
+public class ComponentParameterCaptureUnmatchedValuesHasWrongTypeTest : DiagnosticVerifier
+{
+    [Theory]
+    [InlineData("System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, object>>")]
+    [InlineData("System.Collections.Generic.Dictionary<string, object>")]
+    [InlineData("System.Collections.Generic.IDictionary<string, object>")]
+    [InlineData("System.Collections.Generic.IReadOnlyDictionary<string, object>")]
+    public void IgnoresPropertiesWithSupportedType(string propertyType)
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter(CaptureUnmatchedValues = true)] public {propertyType} MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void IgnoresPropertiesWithCaptureUnmatchedValuesFalse()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter(CaptureUnmatchedValues = false)] public string MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void AddsDiagnosticForInvalidType()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter(CaptureUnmatchedValues = true)] public string MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test,
+                new DiagnosticResult
+                {
+                    Id = DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType.Id,
+                    Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty' defines CaptureUnmatchedValues but has an unsupported type 'string'. Use a type assignable from 'System.Collections.Generic.Dictionary<string, object>'.",
+                    Severity = DiagnosticSeverity.Warning,
+                    Locations = new[]
+                    {
+                        new DiagnosticResultLocation("Test0.cs", 7, 70)
+                    }
+                });
+    }
+
+    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+    {
+        return new ComponentParameterAnalyzer();
+    }
+}

+ 66 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentParameterCaptureUnmatchedValuesMustBeUniqueTest.cs

@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Test;
+
+public class ComponentParameterCaptureUnmatchedValuesMustBeUniqueTest : DiagnosticVerifier
+{
+    [Fact]
+    public void IgnoresPropertiesWithCaptureUnmatchedValuesFalse()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using System.Collections.Generic;
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter(CaptureUnmatchedValues = false)] public string MyProperty {{ get; set; }}
+            [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> MyOtherProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void AddsDiagnosticForMultipleCaptureUnmatchedValuesProperties()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using System.Collections.Generic;
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> MyProperty {{ get; set; }}
+            [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> MyOtherProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        var message = @"Component type 'ConsoleApplication1.TypeName' defines properties multiple parameters with CaptureUnmatchedValues. Properties: " + Environment.NewLine +
+"ConsoleApplication1.TypeName.MyOtherProperty" + Environment.NewLine +
+"ConsoleApplication1.TypeName.MyProperty";
+
+        VerifyCSharpDiagnostic(test,
+                new DiagnosticResult
+                {
+                    Id = DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique.Id,
+                    Message = message,
+                    Severity = DiagnosticSeverity.Warning,
+                    Locations = new[]
+                    {
+                        new DiagnosticResultLocation("Test0.cs", 6, 15)
+                    }
+                });
+    }
+
+    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+    {
+        return new ComponentParameterAnalyzer();
+    }
+}

+ 109 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentParameterSettersShouldBePublicTest.cs

@@ -0,0 +1,109 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+public class ComponentParameterSettersShouldBePublicTest : DiagnosticVerifier
+{
+    [Fact]
+    public void IgnoresCascadingParameterProperties()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(CascadingParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [CascadingParameter] string MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void IgnoresPublicSettersProperties()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter] public string MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void IgnoresPrivateSettersNonParameterProperties()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            private string MyProperty {{ get; private set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void ErrorsForNonPublicSetterParameters()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter] public string MyProperty1 {{ get; private set; }}
+            [Parameter] public string MyProperty2 {{ get; protected set; }}
+            [Parameter] public string MyProperty3 {{ get; internal set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty1' should have a public setter.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 7, 39)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty2' should have a public setter.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 8, 39)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty3' should have a public setter.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 9, 39)
+                }
+            });
+    }
+
+    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new ComponentParameterAnalyzer();
+}

+ 359 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentParameterUsageAnalyzerTest.cs

@@ -0,0 +1,359 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+public class ComponentParameterUsageAnalyzerTest : DiagnosticVerifier
+{
+    public ComponentParameterUsageAnalyzerTest()
+    {
+        ComponentTestSource = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TestComponent : IComponent
+        {{
+            [Parameter] public string TestProperty {{ get; set; }}
+            [Parameter] public int TestInt {{ get; set; }}
+            public string NonParameter {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+    }
+
+    private string ComponentTestSource { get; }
+
+    [Fact]
+    public void ComponentPropertySimpleAssignment_Warns()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private TestComponent _testComponent;
+            void Render()
+            {{
+                _testComponent = new TestComponent();
+                _testComponent.TestProperty = ""Hello World"";
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent.Id,
+                Message = "Component parameter 'TestProperty' should not be set outside of its component.",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 11, 17)
+                }
+            });
+    }
+
+    [Fact]
+    public void ComponentPropertyCoalesceAssignment__Warns()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private TestComponent _testComponent;
+            void Render()
+            {{
+                _testComponent = new TestComponent();
+                _testComponent.TestProperty ??= ""Hello World"";
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent.Id,
+                Message = "Component parameter 'TestProperty' should not be set outside of its component.",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 11, 17)
+                }
+            });
+    }
+
+    [Fact]
+    public void ComponentPropertyCompoundAssignment__Warns()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private TestComponent _testComponent;
+            void Render()
+            {{
+                _testComponent = new TestComponent();
+                _testComponent.TestProperty += ""Hello World"";
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent.Id,
+                Message = "Component parameter 'TestProperty' should not be set outside of its component.",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 11, 17)
+                }
+            });
+    }
+
+    [Fact]
+    public void ComponentPropertyIncrement_Warns()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private TestComponent _testComponent;
+            void Render()
+            {{
+                _testComponent = new TestComponent();
+                _testComponent.TestInt++;
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent.Id,
+                Message = "Component parameter 'TestInt' should not be set outside of its component.",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 11, 17)
+                }
+            });
+    }
+
+    [Fact]
+    public void ComponentPropertyDecrement_Warns()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private TestComponent _testComponent;
+            void Render()
+            {{
+                _testComponent = new TestComponent();
+                _testComponent.TestInt--;
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldNotBeSetOutsideOfTheirDeclaredComponent.Id,
+                Message = "Component parameter 'TestInt' should not be set outside of its component.",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 11, 17)
+                }
+            });
+    }
+
+    [Fact]
+    public void ComponentPropertyExpression_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            void Method()
+            {{
+                System.IO.Console.WriteLine(new TestComponent().TestProperty);
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void ComponentPropertyExpressionInStatement_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            void Method()
+            {{
+                var testComponent = new TestComponent();
+                for (var i = 0; i < testComponent.TestProperty.Length; i++)
+                {{
+                }}
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void RetrievalOfComponentPropertyValueInAssignment_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            void Method()
+            {{
+                var testComponent = new TestComponent();
+                AnotherProperty = testComponent.TestProperty;
+            }}
+
+            public string AnotherProperty {{ get; set; }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void ShadowedComponentPropertyAssignment_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            void Method()
+            {{
+                var testComponent = new InheritedComponent();
+                testComponent.TestProperty = ""Hello World"";
+            }}
+        }}
+
+        class InheritedComponent : TestComponent
+        {{
+            public new string TestProperty {{ get; set; }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void InheritedImplicitComponentPropertyAssignment_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName : TestComponent
+        {{
+            void Method()
+            {{
+                this.TestProperty = ""Hello World"";
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void ImplicitComponentPropertyAssignment_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName : IComponent
+        {{
+            void Method()
+            {{
+                TestProperty = ""Hello World"";
+            }}
+
+            [Parameter] public string TestProperty {{ get; set; }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void ComponentPropertyAssignment_NonParameter_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private TestComponent _testComponent;
+            void Render()
+            {{
+                _testComponent = new TestComponent();
+                _testComponent.NonParameter = ""Hello World"";
+            }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void NonComponentPropertyAssignment_Ignores()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class OtherComponent : IComponent
+        {{
+            private SomethingElse _testNonComponent;
+            void Render()
+            {{
+                _testNonComponent = new NotAComponent();
+                _testNonComponent.TestProperty = ""Hello World"";
+            }}
+        }}
+        class NotAComponent
+        {{
+            [Parameter] public string TestProperty {{ get; set; }}
+        }}
+    }}" + ComponentTestSource;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new ComponentParameterUsageAnalyzer();
+}

+ 124 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentParametersShouldBePublicCodeFixProviderTest.cs

@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Test;
+
+public class ComponentParametersShouldBePublicCodeFixProviderTest : CodeFixVerifier
+{
+    [Fact]
+    public void IgnoresPrivatePropertiesWithoutParameterAttribute()
+    {
+        var test = @"
+    namespace ConsoleApplication1
+    {
+        class TypeName
+        {
+            private string MyProperty { get; set; }
+        }
+    }" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void AddsDiagnosticAndFixForPrivatePropertiesWithParameterAttribute()
+    {
+        var test = @"
+    namespace ConsoleApplication1
+    {
+        using " + typeof(ParameterAttribute).Namespace + @";
+
+        class TypeName
+        {
+            [Parameter] private string BadProperty1 { get; set; }
+        }
+    }" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.BadProperty1' should be public.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 8, 40)
+                }
+            });
+
+        VerifyCSharpFix(test, @"
+    namespace ConsoleApplication1
+    {
+        using " + typeof(ParameterAttribute).Namespace + @";
+
+        class TypeName
+        {
+            [Parameter] public string BadProperty1 { get; set; }
+        }
+    }" + ComponentsTestDeclarations.Source);
+    }
+
+    [Fact]
+    public void IgnoresPublicPropertiesWithNonPublicSetterWithParameterAttribute()
+    {
+        var test = @"
+    namespace ConsoleApplication1
+    {
+        using " + typeof(ParameterAttribute).Namespace + @";
+
+        class TypeName
+        {
+            [Parameter] public string MyProperty1 { get; private set; }
+            [Parameter] public object MyProperty2 { get; protected set; }
+            [Parameter] public object MyProperty3 { get; internal set; }
+        }
+    }" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty1' should have a public setter.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 8, 39)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty2' should have a public setter.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 9, 39)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty3' should have a public setter.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 10, 39)
+                }
+            });
+    }
+
+    protected override CodeFixProvider GetCSharpCodeFixProvider()
+    {
+        return new ComponentParametersShouldBePublicCodeFixProvider();
+    }
+
+    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+    {
+        return new ComponentParameterAnalyzer();
+    }
+}

+ 104 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentParametersShouldBePublicTest.cs

@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+public class ComponentParametersShouldBePublicTest : DiagnosticVerifier
+{
+    [Fact]
+    public void IgnoresPublicProperties()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter] public string MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void IgnoresPrivateNonParameterProperties()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            private string MyProperty {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test);
+    }
+
+    [Fact]
+    public void ErrorsForNonPublicParameters()
+    {
+        var test = $@"
+    namespace ConsoleApplication1
+    {{
+        using {typeof(ParameterAttribute).Namespace};
+        class TypeName
+        {{
+            [Parameter] string MyProperty1 {{ get; set; }}
+            [Parameter] private string MyProperty2 {{ get; set; }}
+            [Parameter] protected string MyProperty3 {{ get; set; }}
+            [Parameter] internal string MyProperty4 {{ get; set; }}
+        }}
+    }}" + ComponentsTestDeclarations.Source;
+
+        VerifyCSharpDiagnostic(test,
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty1' should be public.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 7, 32)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty2' should be public.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 8, 40)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty3' should be public.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 9, 42)
+                }
+            },
+            new DiagnosticResult
+            {
+                Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
+                Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty4' should be public.",
+                Severity = DiagnosticSeverity.Error,
+                Locations = new[]
+                {
+                        new DiagnosticResultLocation("Test0.cs", 10, 41)
+                }
+            });
+    }
+
+    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new ComponentParameterAnalyzer();
+}

+ 25 - 0
src/Tools/SDK-Analyzers/Components/test/ComponentsTestDeclarations.cs

@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Components.Analyzers;
+
+public static class ComponentsTestDeclarations
+{
+    public static readonly string Source = $@"
+    namespace {typeof(ParameterAttribute).Namespace}
+    {{
+        public class {typeof(ParameterAttribute).Name} : System.Attribute
+        {{
+            public bool CaptureUnmatchedValues {{ get; set; }}
+        }}
+
+        public class {typeof(CascadingParameterAttribute).Name} : System.Attribute
+        {{
+        }}
+
+        public interface {typeof(IComponent).Name}
+        {{
+        }}
+    }}
+";
+}

+ 86 - 0
src/Tools/SDK-Analyzers/Components/test/Helpers/CodeFixVerifier.Helper.cs

@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Simplification;
+
+namespace TestHelper;
+
+/// <summary>
+/// Diagnostic Producer class with extra methods dealing with applying codefixes
+/// All methods are static
+/// </summary>
+public abstract partial class CodeFixVerifier : DiagnosticVerifier
+{
+    /// <summary>
+    /// Apply the inputted CodeAction to the inputted document.
+    /// Meant to be used to apply codefixes.
+    /// </summary>
+    /// <param name="document">The Document to apply the fix on</param>
+    /// <param name="codeAction">A CodeAction that will be applied to the Document.</param>
+    /// <returns>A Document with the changes from the CodeAction</returns>
+    private static Document ApplyFix(Document document, CodeAction codeAction)
+    {
+        var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
+        var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
+        return solution.GetDocument(document.Id);
+    }
+
+    /// <summary>
+    /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection.
+    /// Note: Considers Diagnostics to be the same if they have the same Ids.  In the case of multiple diagnostics with the same Id in a row,
+    /// this method may not necessarily return the new one.
+    /// </summary>
+    /// <param name="diagnostics">The Diagnostics that existed in the code before the CodeFix was applied</param>
+    /// <param name="newDiagnostics">The Diagnostics that exist in the code after the CodeFix was applied</param>
+    /// <returns>A list of Diagnostics that only surfaced in the code after the CodeFix was applied</returns>
+    private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics)
+    {
+        var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
+        var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
+
+        int oldIndex = 0;
+        int newIndex = 0;
+
+        while (newIndex < newArray.Length)
+        {
+            if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id)
+            {
+                ++oldIndex;
+                ++newIndex;
+            }
+            else
+            {
+                yield return newArray[newIndex++];
+            }
+        }
+    }
+
+    /// <summary>
+    /// Get the existing compiler diagnostics on the inputted document.
+    /// </summary>
+    /// <param name="document">The Document to run the compiler diagnostic analyzers on</param>
+    /// <returns>The compiler diagnostics that were found in the code</returns>
+    private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
+    {
+        return document.GetSemanticModelAsync().Result.GetDiagnostics();
+    }
+
+    /// <summary>
+    /// Given a document, turn it into a string based on the syntax root
+    /// </summary>
+    /// <param name="document">The Document to be converted to a string</param>
+    /// <returns>A string containing the syntax of the Document after formatting</returns>
+    private static string GetStringFromDocument(Document document)
+    {
+        var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
+        var root = simplifiedDoc.GetSyntaxRootAsync().Result;
+        root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
+        return root.GetText().ToString();
+    }
+}
+

+ 90 - 0
src/Tools/SDK-Analyzers/Components/test/Helpers/DiagnosticResult.cs

@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+
+namespace TestHelper;
+
+/// <summary>
+/// Location where the diagnostic appears, as determined by path, line number, and column number.
+/// </summary>
+public struct DiagnosticResultLocation
+{
+    public DiagnosticResultLocation(string path, int line, int column)
+    {
+        if (line < -1)
+        {
+            throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
+        }
+
+        if (column < -1)
+        {
+            throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
+        }
+
+        this.Path = path;
+        this.Line = line;
+        this.Column = column;
+    }
+
+    public string Path { get; }
+    public int Line { get; }
+    public int Column { get; }
+}
+
+/// <summary>
+/// Struct that stores information about a Diagnostic appearing in a source
+/// </summary>
+public struct DiagnosticResult
+{
+    private DiagnosticResultLocation[] locations;
+
+    public DiagnosticResultLocation[] Locations
+    {
+        get
+        {
+            if (this.locations == null)
+            {
+                this.locations = new DiagnosticResultLocation[] { };
+            }
+            return this.locations;
+        }
+
+        set
+        {
+            this.locations = value;
+        }
+    }
+
+    public DiagnosticSeverity Severity { get; set; }
+
+    public string Id { get; set; }
+
+    public string Message { get; set; }
+
+    public string Path
+    {
+        get
+        {
+            return this.Locations.Length > 0 ? this.Locations[0].Path : "";
+        }
+    }
+
+    public int Line
+    {
+        get
+        {
+            return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
+        }
+    }
+
+    public int Column
+    {
+        get
+        {
+            return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
+        }
+    }
+}

+ 171 - 0
src/Tools/SDK-Analyzers/Components/test/Helpers/DiagnosticVerifier.Helper.cs

@@ -0,0 +1,171 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+
+namespace TestHelper;
+
+/// <summary>
+/// Class for turning strings into documents and getting the diagnostics on them
+/// All methods are static
+/// </summary>
+public abstract partial class DiagnosticVerifier
+{
+    private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
+    private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
+    private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location);
+    private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location);
+
+    internal static string DefaultFilePathPrefix = "Test";
+    internal static string CSharpDefaultFileExt = "cs";
+    internal static string VisualBasicDefaultExt = "vb";
+    internal static string TestProjectName = "TestProject";
+
+    #region  Get Diagnostics
+
+    /// <summary>
+    /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
+    /// </summary>
+    /// <param name="sources">Classes in the form of strings</param>
+    /// <param name="language">The language the source classes are in</param>
+    /// <param name="analyzer">The analyzer to be run on the sources</param>
+    /// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
+    private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
+    {
+        return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
+    }
+
+    /// <summary>
+    /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
+    /// The returned diagnostics are then ordered by location in the source document.
+    /// </summary>
+    /// <param name="analyzer">The analyzer to run on the documents</param>
+    /// <param name="documents">The Documents that the analyzer will be run on</param>
+    /// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
+    protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
+    {
+        var projects = new HashSet<Project>();
+        foreach (var document in documents)
+        {
+            projects.Add(document.Project);
+        }
+
+        var diagnostics = new List<Diagnostic>();
+        foreach (var project in projects)
+        {
+            var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
+            var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
+            foreach (var diag in diags)
+            {
+                if (diag.Location == Location.None || diag.Location.IsInMetadata)
+                {
+                    diagnostics.Add(diag);
+                }
+                else
+                {
+                    for (int i = 0; i < documents.Length; i++)
+                    {
+                        var document = documents[i];
+                        var tree = document.GetSyntaxTreeAsync().Result;
+                        if (tree == diag.Location.SourceTree)
+                        {
+                            diagnostics.Add(diag);
+                        }
+                    }
+                }
+            }
+        }
+
+        var results = SortDiagnostics(diagnostics);
+        diagnostics.Clear();
+        return results;
+    }
+
+    /// <summary>
+    /// Sort diagnostics by location in source document
+    /// </summary>
+    /// <param name="diagnostics">The list of Diagnostics to be sorted</param>
+    /// <returns>An IEnumerable containing the Diagnostics in order of Location</returns>
+    private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics)
+    {
+        return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
+    }
+
+    #endregion
+
+    #region Set up compilation and documents
+    /// <summary>
+    /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
+    /// </summary>
+    /// <param name="sources">Classes in the form of strings</param>
+    /// <param name="language">The language the source code is in</param>
+    /// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns>
+    private static Document[] GetDocuments(string[] sources, string language)
+    {
+        if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
+        {
+            throw new ArgumentException("Unsupported Language");
+        }
+
+        var project = CreateProject(sources, language);
+        var documents = project.Documents.ToArray();
+
+        if (sources.Length != documents.Length)
+        {
+            throw new InvalidOperationException("Amount of sources did not match amount of Documents created");
+        }
+
+        return documents;
+    }
+
+    /// <summary>
+    /// Create a Document from a string through creating a project that contains it.
+    /// </summary>
+    /// <param name="source">Classes in the form of a string</param>
+    /// <param name="language">The language the source code is in</param>
+    /// <returns>A Document created from the source string</returns>
+    protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
+    {
+        return CreateProject(new[] { source }, language).Documents.First();
+    }
+
+    /// <summary>
+    /// Create a project using the inputted strings as sources.
+    /// </summary>
+    /// <param name="sources">Classes in the form of strings</param>
+    /// <param name="language">The language the source code is in</param>
+    /// <returns>A Project created out of the Documents created from the source strings</returns>
+    private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
+    {
+        string fileNamePrefix = DefaultFilePathPrefix;
+        string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
+
+        var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
+
+        var solution = new AdhocWorkspace()
+            .CurrentSolution
+            .AddProject(projectId, TestProjectName, TestProjectName, language)
+            .AddMetadataReference(projectId, CorlibReference)
+            .AddMetadataReference(projectId, SystemCoreReference)
+            .AddMetadataReference(projectId, CSharpSymbolsReference)
+            .AddMetadataReference(projectId, CodeAnalysisReference);
+
+        int count = 0;
+        foreach (var source in sources)
+        {
+            var newFileName = fileNamePrefix + count + "." + fileExt;
+            var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
+            solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
+            count++;
+        }
+        return solution.GetProject(projectId);
+    }
+    #endregion
+}
+

+ 22 - 0
src/Tools/SDK-Analyzers/Components/test/Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <PreserveCompilationContext>true</PreserveCompilationContext>
+    <BaseOutputPath />
+    <NoWarn>$(NoWarn);IDE0055;IDE0161</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <!-- This is set to a ProjectReference because analyzers cannot be referenced via Reference.  -->
+    <ProjectReference Include="..\src\Microsoft.AspNetCore.Components.SdkAnalyzers.csproj" />
+    <ProjectReference Include="$(RepoRoot)src\Analyzers\Microsoft.AspNetCore.Analyzer.Testing\src\Microsoft.AspNetCore.Analyzer.Testing.csproj" />
+
+    <Reference Include="Microsoft.AspNetCore.Components" />
+    <Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="TestFiles\**\*.*" CopyToPublishDirectory="PreserveNewest" />
+  </ItemGroup>
+</Project>

+ 23 - 0
src/Tools/SDK-Analyzers/Components/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsersRendererTypesInMethodBody.cs

@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.AspNetCore.Components.RenderTree;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnosticsAnalyzerTest
+{
+    class UsersRendererTypesInMethodBody
+    {
+        private void Test()
+        {
+            var test = /*MMField*/RenderTreeFrameType.Attribute;
+            GC.KeepAlive(test);
+
+            var frame = /*MMNewObject*/new RenderTreeFrame();
+            GC.KeepAlive(/*MMProperty*/frame.Component);
+
+            var range = /*MMNewObject2*/new ArrayRange<string>(null, 0);
+            /*MMInvocation*/range.Clone();
+        }
+    }
+}

+ 28 - 0
src/Tools/SDK-Analyzers/Components/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRendererAsBaseClass.cs

@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.RenderTree;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnosticsAnalyzerTest
+{
+    /*MM*/
+    class UsesRendererAsBaseClass : Renderer
+    {
+        public UsesRendererAsBaseClass()
+            : base(null, null)
+        {
+        }
+
+        public override Dispatcher Dispatcher => throw new NotImplementedException();
+
+        protected override void HandleException(Exception exception)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override Task UpdateDisplayAsync(/*M1*/in RenderBatch renderBatch)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 39 - 0
src/Tools/SDK-Analyzers/Components/test/TestFiles/ComponentInternalUsageDiagnosticsAnalyzerTest/UsesRendererTypesInDeclarations.cs

@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.RenderTree;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnosticsAnalyzerTest
+{
+    /*MMBaseClass*/class UsesRendererTypesInDeclarations : Renderer
+    {
+        private readonly Renderer /*MMField*/_field = null;
+
+        public UsesRendererTypesInDeclarations()
+            /*MMInvocation*/: base(null, null)
+        {
+        }
+
+        public override Dispatcher Dispatcher => throw new NotImplementedException();
+
+        /*MMProperty*/public Renderer Property { get; set; }
+
+        protected override void HandleException(Exception exception)
+        {
+            throw new NotImplementedException();
+        }
+
+        /*MMParameter*/protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
+        {
+            throw new NotImplementedException();
+        }
+
+        /*MMReturnType*/private Renderer GetRenderer() => _field;
+
+        public interface ITestInterface
+        {
+        }
+    }
+}

+ 131 - 0
src/Tools/SDK-Analyzers/Components/test/Verifiers/CodeFixVerifier.cs

@@ -0,0 +1,131 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using System.Globalization;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Formatting;
+
+namespace TestHelper;
+
+/// <summary>
+/// Superclass of all Unit tests made for diagnostics with codefixes.
+/// Contains methods used to verify correctness of codefixes
+/// </summary>
+public abstract partial class CodeFixVerifier : DiagnosticVerifier
+{
+    /// <summary>
+    /// Returns the codefix being tested (C#) - to be implemented in non-abstract class
+    /// </summary>
+    /// <returns>The CodeFixProvider to be used for CSharp code</returns>
+    protected virtual CodeFixProvider GetCSharpCodeFixProvider()
+    {
+        return null;
+    }
+
+    /// <summary>
+    /// Returns the codefix being tested (VB) - to be implemented in non-abstract class
+    /// </summary>
+    /// <returns>The CodeFixProvider to be used for VisualBasic code</returns>
+    protected virtual CodeFixProvider GetBasicCodeFixProvider()
+    {
+        return null;
+    }
+
+    /// <summary>
+    /// Called to test a C# codefix when applied on the inputted string as a source
+    /// </summary>
+    /// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
+    /// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
+    /// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
+    /// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
+    protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
+    {
+        VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
+    }
+
+    /// <summary>
+    /// Called to test a VB codefix when applied on the inputted string as a source
+    /// </summary>
+    /// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
+    /// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
+    /// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
+    /// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
+    protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
+    {
+        VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
+    }
+
+    /// <summary>
+    /// General verifier for codefixes.
+    /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes.
+    /// Then gets the string after the codefix is applied and compares it with the expected result.
+    /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true.
+    /// </summary>
+    /// <param name="language">The language the source code is in</param>
+    /// <param name="analyzer">The analyzer to be applied to the source code</param>
+    /// <param name="codeFixProvider">The codefix to be applied to the code wherever the relevant Diagnostic is found</param>
+    /// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
+    /// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
+    /// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
+    /// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
+    private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics)
+    {
+        var document = CreateDocument(oldSource, language);
+        var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
+        var compilerDiagnostics = GetCompilerDiagnostics(document);
+        var attempts = analyzerDiagnostics.Length;
+
+        for (int i = 0; i < attempts; ++i)
+        {
+            var actions = new List<CodeAction>();
+            var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
+            codeFixProvider.RegisterCodeFixesAsync(context).Wait();
+
+            if (!actions.Any())
+            {
+                break;
+            }
+
+            if (codeFixIndex != null)
+            {
+                document = ApplyFix(document, actions.ElementAt((int)codeFixIndex));
+                break;
+            }
+
+            document = ApplyFix(document, actions.ElementAt(0));
+            analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
+
+            var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
+
+            //check if applying the code fix introduced any new compiler diagnostics
+            if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any())
+            {
+                // Format and get the compiler diagnostics again so that the locations make sense in the output
+                document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace));
+                newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
+
+                Assert.True(false,
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n",
+                        string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())),
+                        document.GetSyntaxRootAsync().Result.ToFullString()));
+            }
+
+            //check if there are analyzer diagnostics left after the code fix
+            if (!analyzerDiagnostics.Any())
+            {
+                break;
+            }
+        }
+
+        //after applying all of the code fixes, compare the resulting string to the inputted one
+        var actual = GetStringFromDocument(document);
+        Assert.Equal(newSource, actual);
+    }
+}

+ 287 - 0
src/Tools/SDK-Analyzers/Components/test/Verifiers/DiagnosticVerifier.cs

@@ -0,0 +1,287 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using System.Globalization;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace TestHelper;
+
+/// <summary>
+/// Superclass of all Unit Tests for DiagnosticAnalyzers
+/// </summary>
+public abstract partial class DiagnosticVerifier
+{
+    #region To be implemented by Test classes
+    /// <summary>
+    /// Get the CSharp analyzer being tested - to be implemented in non-abstract class
+    /// </summary>
+    protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+    {
+        return null;
+    }
+
+    /// <summary>
+    /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
+    /// </summary>
+    protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
+    {
+        return null;
+    }
+    #endregion
+
+    #region Verifier wrappers
+
+    /// <summary>
+    /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
+    /// Note: input a DiagnosticResult for each Diagnostic expected
+    /// </summary>
+    /// <param name="source">A class in the form of a string to run the analyzer on</param>
+    /// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
+    protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
+    {
+        VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
+    }
+
+    /// <summary>
+    /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source
+    /// Note: input a DiagnosticResult for each Diagnostic expected
+    /// </summary>
+    /// <param name="source">A class in the form of a string to run the analyzer on</param>
+    /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the source</param>
+    protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected)
+    {
+        VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
+    }
+
+    /// <summary>
+    /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
+    /// Note: input a DiagnosticResult for each Diagnostic expected
+    /// </summary>
+    /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
+    /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
+    protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
+    {
+        VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
+    }
+
+    /// <summary>
+    /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source
+    /// Note: input a DiagnosticResult for each Diagnostic expected
+    /// </summary>
+    /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
+    /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
+    protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected)
+    {
+        VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
+    }
+
+    /// <summary>
+    /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
+    /// then verifies each of them.
+    /// </summary>
+    /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
+    /// <param name="language">The language of the classes represented by the source strings</param>
+    /// <param name="analyzer">The analyzer to be run on the source code</param>
+    /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
+    private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
+    {
+        var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
+        VerifyDiagnosticResults(diagnostics, analyzer, expected);
+    }
+
+    #endregion
+
+    #region Actual comparisons and verifications
+    /// <summary>
+    /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
+    /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
+    /// </summary>
+    /// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
+    /// <param name="analyzer">The analyzer that was being run on the sources</param>
+    /// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
+    private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
+    {
+        int expectedCount = expectedResults.Length;
+        int actualCount = actualResults.Count();
+
+        if (expectedCount != actualCount)
+        {
+            string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : "    NONE.";
+
+            Assert.True(false,
+                string.Format(CultureInfo.InvariantCulture, "Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
+        }
+
+        for (int i = 0; i < expectedResults.Length; i++)
+        {
+            var actual = actualResults.ElementAt(i);
+            var expected = expectedResults[i];
+
+            if (expected.Line == -1 && expected.Column == -1)
+            {
+                if (actual.Location != Location.None)
+                {
+                    Assert.True(false,
+                        string.Format(CultureInfo.InvariantCulture, "Expected:\nA project diagnostic with No location\nActual:\n{0}",
+                        FormatDiagnostics(analyzer, actual)));
+                }
+            }
+            else
+            {
+                VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
+                var additionalLocations = actual.AdditionalLocations.ToArray();
+
+                if (additionalLocations.Length != expected.Locations.Length - 1)
+                {
+                    Assert.True(false,
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "Expected {0} additional locations but got {1} for Diagnostic:\r\n    {2}\r\n",
+                            expected.Locations.Length - 1, additionalLocations.Length,
+                            FormatDiagnostics(analyzer, actual)));
+                }
+
+                for (int j = 0; j < additionalLocations.Length; ++j)
+                {
+                    VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
+                }
+            }
+
+            if (actual.Id != expected.Id)
+            {
+                Assert.True(false,
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n    {2}\r\n",
+                        expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
+            }
+
+            if (actual.Severity != expected.Severity)
+            {
+                Assert.True(false,
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n    {2}\r\n",
+                        expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
+            }
+
+            if (actual.GetMessage() != expected.Message)
+            {
+                Assert.True(false,
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n    {2}\r\n",
+                        expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
+            }
+        }
+    }
+
+    /// <summary>
+    /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
+    /// </summary>
+    /// <param name="analyzer">The analyzer that was being run on the sources</param>
+    /// <param name="diagnostic">The diagnostic that was found in the code</param>
+    /// <param name="actual">The Location of the Diagnostic found in the code</param>
+    /// <param name="expected">The DiagnosticResultLocation that should have been found</param>
+    private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
+    {
+        var actualSpan = actual.GetLineSpan();
+
+        Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
+            string.Format(
+                CultureInfo.InvariantCulture,
+                "Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n    {2}\r\n",
+                expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
+
+        var actualLinePosition = actualSpan.StartLinePosition;
+
+        // Only check line position if there is an actual line in the real diagnostic
+        if (actualLinePosition.Line > 0)
+        {
+            if (actualLinePosition.Line + 1 != expected.Line)
+            {
+                Assert.True(false,
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n    {2}\r\n",
+                        expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
+            }
+        }
+
+        // Only check column position if there is an actual column position in the real diagnostic
+        if (actualLinePosition.Character > 0)
+        {
+            if (actualLinePosition.Character + 1 != expected.Column)
+            {
+                Assert.True(false,
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n    {2}\r\n",
+                        expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
+            }
+        }
+    }
+    #endregion
+
+    #region Formatting Diagnostics
+    /// <summary>
+    /// Helper method to format a Diagnostic into an easily readable string
+    /// </summary>
+    /// <param name="analyzer">The analyzer that this verifier tests</param>
+    /// <param name="diagnostics">The Diagnostics to be formatted</param>
+    /// <returns>The Diagnostics formatted as a string</returns>
+    private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
+    {
+        var builder = new StringBuilder();
+        for (int i = 0; i < diagnostics.Length; ++i)
+        {
+            builder.AppendLine("// " + diagnostics[i].ToString());
+
+            var analyzerType = analyzer.GetType();
+            var rules = analyzer.SupportedDiagnostics;
+
+            foreach (var rule in rules)
+            {
+                if (rule != null && rule.Id == diagnostics[i].Id)
+                {
+                    var location = diagnostics[i].Location;
+                    if (location == Location.None)
+                    {
+                        builder.AppendFormat(CultureInfo.InvariantCulture, "GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
+                    }
+                    else
+                    {
+                        Assert.True(location.IsInSource,
+                            $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
+
+                        string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs", StringComparison.Ordinal) ? "GetCSharpResultAt" : "GetBasicResultAt";
+                        var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
+
+                        builder.AppendFormat(
+                            CultureInfo.InvariantCulture,
+                            "{0}({1}, {2}, {3}.{4})",
+                            resultMethodName,
+                            linePosition.Line + 1,
+                            linePosition.Character + 1,
+                            analyzerType.Name,
+                            rule.Id);
+                    }
+
+                    if (i != diagnostics.Length - 1)
+                    {
+                        builder.Append(',');
+                    }
+
+                    builder.AppendLine();
+                    break;
+                }
+            }
+        }
+        return builder.ToString();
+    }
+    #endregion
+}

+ 3 - 0
src/Tools/SDK-Analyzers/README.md

@@ -0,0 +1,3 @@
+### SDK Analyzers
+
+ASP.NET Core analyzers that are shipped as part of Microsoft.NET.Sdk.Web in the .NET SDK. These analyzers apply uniformly to 3.1, 5.0, and 6.0 apps. We want to avoid introducing new diagnostics in these analyzers since it would result in new warnings / errors in working apps.