diff --git a/Winforms.sln b/Winforms.sln index 5107185a3af..4232199c7a1 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -186,6 +186,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Windows.Forms.Analyz EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "System.Windows.Forms.Analyzers.VisualBasic.Tests", "src\System.Windows.Forms.Analyzers\vb\tests\System.Windows.Forms.Analyzers.VisualBasic.Tests.vbproj", "{ACF7ACC1-5163-8728-DEA7-7758DF20738E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Prompting", "Prompting", "{681B7522-35D1-4D58-8956-6E5E26D461B2}" + ProjectSection(SolutionItems) = preProject + src\System.Windows.Forms.Analyzers\prompting\AnalyzerTests-Copilot-Instructions.md = src\System.Windows.Forms.Analyzers\prompting\AnalyzerTests-Copilot-Instructions.md + src\System.Windows.Forms.Analyzers\prompting\SamplePrompt for AnalyzerTests.md = src\System.Windows.Forms.Analyzers\prompting\SamplePrompt for AnalyzerTests.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1121,6 +1127,7 @@ Global {6D3F4979-A444-778A-B6ED-6AA1786DADA0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {1D7A95BF-545D-8D63-3CD1-75619B80A2A0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {ACF7ACC1-5163-8728-DEA7-7758DF20738E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {681B7522-35D1-4D58-8956-6E5E26D461B2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B1B0433-F612-4E5A-BE7E-FCF5B9F6E136} diff --git a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/CodeTestDataAttribute.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/CodeTestDataAttribute.cs index 85f0e0dc0e2..3f54c140f1e 100644 --- a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/CodeTestDataAttribute.cs +++ b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/CodeTestDataAttribute.cs @@ -90,14 +90,12 @@ public override IEnumerable GetData(MethodInfo testMethod) ?? throw new InvalidOperationException( $"The type '{baseType}' does not contain a static public method named 'GetFileSets'."); - // Invoke that method to get the object array: IEnumerable fileSets = (IEnumerable) (getFileSetsMethod.Invoke(null, null) ?? throw new InvalidOperationException("GetFileSets method returned null or a value that couldn't be cast to IEnumerable.")); // This is the data, which the test class directs itself to get. var baseData = base.GetData(testMethod).ToList(); - // Use LINQ to create the cross product more efficiently return baseData .SelectMany(referenceAssembly => fileSets.Select(fileSet => new object[] { referenceAssembly[0], fileSet })); diff --git a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/NetVersion.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/NetVersion.cs index e800a488d8e..a828e78d2ba 100644 --- a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/NetVersion.cs +++ b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/NetVersion.cs @@ -3,10 +3,26 @@ namespace System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms; +[Flags] internal enum NetVersion { - Net6_0, - Net7_0, - Net8_0, - Net9_0 + Net6_0 = 0x00000006, + Net7_0 = 0x00000007, + Net8_0 = 0x00000008, + Net9_0 = 0x00000009, + Net10_0 = 0x0000000A, + Net11_0 = 0x0000000B, + Net12_0 = 0x0000000C, + + /// + /// If this is selected, we're taking WinForms runtime build from + /// this repo and the .NET version, the runtime is build against. + /// + WinFormsBuild = 0x01000000, + + /// + /// If this is OR'ed in, we're taking the specified runtime version, + /// and the WinForms runtime for this repo. + /// + BuildOutput = 0x10000000 } diff --git a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.WinFormsReferencesFactory.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.WinFormsReferencesFactory.cs new file mode 100644 index 00000000000..801af59eecf --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.WinFormsReferencesFactory.cs @@ -0,0 +1,335 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Security; +using System.Text.Json; +using System.Text.Json.Nodes; + +using Microsoft.CodeAnalysis.Testing; + +// File-Cherry-Picked and modified a bit from Tanya Solyanik's Commit ec6a9f8, +// PR #12860 for back-port (release/net9-Servicing) purposes. +internal static partial class ReferenceAssemblyGenerator +{ + /// + /// Provides access to the Microsoft.NETCore.App.Ref package this repo is built against. + /// By default Roslyn SDK loads packages from NuGet.org, however, we build + /// against the pre-release versions that might not be available there, we need Roslyn tooling to + /// use our repo's NuGet.config. + /// + /// + /// + /// This class locates the repository root directory by finding the global.json file, + /// then determines the correct .NET Core version and reference assemblies to use for testing. + /// It provides paths to reference assemblies and configuration settings needed for the + /// test infrastructure. + /// + /// + public static class WinFormsReferencesFactory + { + private const string RefPackageName = "Microsoft.NETCore.App.Ref"; + private const string PrivatePackagePath = "artifacts\\packages\\Debug\\NonShipping\\Microsoft.Private.Winforms.9.0.3-dev.nupkg"; + + /// + /// Gets the Target Framework Moniker for the current repository. + /// + public static string? Tfm { get; } + + /// + /// Gets the exact version of the .NET Core reference assemblies being used. + /// + public static string? NetCoreRefsVersion { get; } + + /// + /// Reference assemblies for the .NET Core App that this repo is built against. + /// To be used with the latest public surface defined in assemblies built in this repo. + /// + public static ReferenceAssemblies? NetCoreAppReferences { get; } + + /// + /// Path to the System.Windows.Forms.dll reference assembly in our artifacts folder. + /// It has the latest public API surface area. + /// + public static string? WinFormsRefPath { get; } + + /// + /// Path to the NuGet including the latest build. + /// + public static string? WinFormsPrivatePackagePath => + Path.Join( + RepoRootPath, + PrivatePackagePath); + + /// + /// Gets the root path of the repository. + /// + public static string? RepoRootPath { get; } + + static WinFormsReferencesFactory() + { + if (!GetRootFolderPath(out string? rootFolderPath)) + { + return; + } + + RepoRootPath = rootFolderPath; + + if (!TryGetNetCoreVersion(rootFolderPath, out string? tfm, out string? netCoreRefsVersion)) + { + return; + } + + Tfm = tfm; + + string configuration = +#if DEBUG + "Debug"; +#else + "Release"; +#endif + + WinFormsRefPath = Path.Join( + RepoRootPath, + "artifacts", + "obj", + "System.Windows.Forms", + configuration, + tfm, + "ref", + "System.Windows.Forms.dll"); + + // Specify absolute path to the reference assemblies because this version is not necessarily available in the nuget packages cache. + string netCoreAppRefPath = Path.Join(RepoRootPath, ".dotnet", "packs", RefPackageName); + + if (!Directory.Exists(Path.Join(netCoreAppRefPath, netCoreRefsVersion))) + { + try + { + netCoreRefsVersion = GetAvailableVersion( + netCoreAppRefPath: netCoreAppRefPath, + major: $"{netCoreRefsVersion.Split('.')[0]}."); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Debug.WriteLine($"Error accessing version directories: {ex.Message}"); + return; + } + } + + NetCoreRefsVersion = netCoreRefsVersion; + + // Get package from our feeds. + NetCoreAppReferences = new ReferenceAssemblies( + targetFramework: tfm, + referenceAssemblyPackage: new PackageIdentity(RefPackageName, netCoreRefsVersion), + referenceAssemblyPath: Path.Join("ref", tfm)) + .WithNuGetConfigFilePath(Path.Join(RepoRootPath, "NuGet.config")); + } + + /// + /// Gets an available .NET Core version by searching for directories matching a major version prefix. + /// + /// Path to the directory containing .NET Core reference assemblies. + /// Major version prefix to search for (e.g., "6."). + /// The full version string of the matching directory. + /// Thrown when no matching version directory is found. + private static string GetAvailableVersion(string netCoreAppRefPath, string major) + { + if (!Directory.Exists(netCoreAppRefPath)) + { + throw new DirectoryNotFoundException($"Reference assembly directory not found: {netCoreAppRefPath}"); + } + + string[] versions = Directory.GetDirectories(netCoreAppRefPath); + string? availableVersion = versions.FirstOrDefault(v => + Path.GetFileName(v).StartsWith(major, StringComparison.InvariantCultureIgnoreCase)); + + return availableVersion is null + ? throw new DirectoryNotFoundException($"No matching version directory found for major version {major} in {netCoreAppRefPath}") + : Path.GetFileName(availableVersion); + } + + /// + /// Attempts to get the .NET Core version information from the repository. + /// + /// The repository root path. + /// When successful, contains the Target Framework Moniker. + /// When successful, contains the .NET Core reference version. + /// True if both TFM and version were successfully retrieved; otherwise, false. + private static bool TryGetNetCoreVersion( + string rootFolderPath, + [NotNullWhen(true)] out string? tfm, + [NotNullWhen(true)] out string? netCoreRefsVersion) + { + tfm = default; + netCoreRefsVersion = default; + + if (!TryGetSdkVersion(rootFolderPath, out string? version)) + { + return false; + } + + // First, try to use the local .NET SDK if it's there. + string sdkFolderPath = Path.Join(rootFolderPath, ".dotnet", "sdk", version); + + if (!Directory.Exists(sdkFolderPath)) + { + Debug.WriteLine($"SDK folder not found: {sdkFolderPath}"); + return false; + } + + return TryGetNetCoreVersionFromJson(sdkFolderPath, out tfm, out netCoreRefsVersion); + } + + /// + /// Attempts to find the repository root folder by searching for global.json. + /// + /// When successful, contains the path to the repository root. + /// True if the root folder was found; otherwise, false. + private static bool GetRootFolderPath([NotNullWhen(true)] out string? root) + { + root = default; + + try + { + // Our tests should be running from somewhere within the repo root. + // So, we walk the parent folder structure until we find our global.json. + string? testPath = Path.GetDirectoryName(typeof(WinFormsReferencesFactory).Assembly.Location); + + if (testPath is null) + { + Debug.WriteLine("Unable to determine assembly location path"); + return false; + } + + // We walk the parent folder structure until we find our global.json. + string? currentFolderPath = Path.GetDirectoryName(testPath); + + while (currentFolderPath is not null) + { + string globalJsonPath = Path.Join(currentFolderPath, "global.json"); + + if (File.Exists(globalJsonPath)) + { + // We've found the repo root. + root = currentFolderPath; + return true; + } + + currentFolderPath = Path.GetDirectoryName(currentFolderPath); + } + + // Either we couldn't determine the assembly location or global.json file couldn't be found. + Debug.WriteLine("Unable to find global.json in any parent directory"); + + return false; + } + catch (Exception ex) when (ex is IOException or SecurityException) + { + Debug.WriteLine($"Error accessing file system when searching for root folder: {ex.Message}"); + return false; + } + } + + /// + /// Attempts to get the SDK version from the global.json file. + /// + /// The repository root path. + /// When successful, contains the SDK version. + /// True if the SDK version was successfully retrieved; otherwise, false. + /// Thrown when global.json file does not exist. + /// Thrown when global.json has invalid format. + private static bool TryGetSdkVersion(string rootFolderPath, [NotNullWhen(true)] out string? version) + { + version = default; + string globalJsonPath = Path.Join(rootFolderPath, "global.json"); + + if (!File.Exists(globalJsonPath)) + { + Debug.WriteLine($"global.json file not found at: {globalJsonPath}"); + return false; + } + + try + { + string globalJsonString = File.ReadAllText(globalJsonPath); + JsonObject? jsonObject = JsonNode.Parse(globalJsonString)?.AsObject(); + version = (string?)jsonObject?["sdk"]?["version"]; + + if (version is null) + { + Debug.WriteLine("SDK version not found in global.json"); + } + + return version is not null; + } + catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException) + { + Debug.WriteLine($"Error reading or parsing global.json: {ex.Message}"); + return false; + } + } + + /// + /// Attempts to get the .NET Core version from the runtime configuration JSON file. + /// + /// Path to the SDK folder. + /// When successful, contains the Target Framework Moniker. + /// When successful, contains the .NET Core version. + /// True if both TFM and version were successfully retrieved; otherwise, false. + /// Thrown when the runtime config file is not found. + /// Thrown when JSON parsing fails. + private static bool TryGetNetCoreVersionFromJson( + string sdkFolderPath, + [NotNullWhen(true)] out string? tfm, + [NotNullWhen(true)] out string? version) + { + tfm = default; + version = default; + + string configJsonPath = Path.Join(sdkFolderPath, "dotnet.runtimeconfig.json"); + + if (!File.Exists(configJsonPath)) + { + Debug.WriteLine($"Runtime config JSON file not found at: {configJsonPath}"); + return false; + } + + try + { + string configJsonString = File.ReadAllText(configJsonPath); + JsonObject? jsonObject = JsonNode.Parse(configJsonString)?.AsObject(); + JsonNode? runtimeOptions = jsonObject?["runtimeOptions"]; + + tfm = (string?)runtimeOptions?["tfm"]; + + if (tfm is null) + { + Debug.WriteLine("TFM not found in runtime config JSON"); + version = default; + return false; + } + + version = (string?)runtimeOptions?["framework"]?["version"]; + + if (version is null) + { + Debug.WriteLine("Framework version not found in runtime config JSON"); + tfm = null; + return false; + } + + return true; + } + catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException) + { + Debug.WriteLine($"Error reading or parsing runtime config JSON: {ex.Message}"); + tfm = null; + version = null; + return false; + } + } + } +} diff --git a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.cs index 635bee61bc1..44019e25a1e 100644 --- a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.cs +++ b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.cs @@ -1,6 +1,7 @@ // 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.Windows.Forms.Analyzers.Tests.Microsoft.WinForms; using Microsoft.CodeAnalysis.Testing; @@ -19,19 +20,100 @@ /// (Microsoft.WindowsDesktop.App.Ref) to ensure tests have access to all required references. /// /// -internal static class ReferenceAssemblyGenerator +internal static partial class ReferenceAssemblyGenerator { private const string NetRuntimeIdentity = "Microsoft.NETCore.App.Ref"; private const string NetDesktopIdentity = "Microsoft.WindowsDesktop.App.Ref"; private static readonly Dictionary s_exactNetVersionLookup = new() { + // These are the runtime versions of .net, we're current support. + // Note that these versions are pulled in at test time into the AdHoc workspace, + // so they do not impose any versioning mismatch to the test assemblies. + // We cannot reliably use what's offered through Microsoft.CodeAnalysis.Testing, + // because _compatible_ version of that package might not provide the runtime/ + // desktop packages we need. [NetVersion.Net6_0] = ("net6.0", "6.0.36"), [NetVersion.Net7_0] = ("net7.0", "7.0.20"), [NetVersion.Net8_0] = ("net8.0", "8.0.13"), [NetVersion.Net9_0] = ("net9.0", "9.0.2") }; + private static readonly List s_winFormsAssemblies = new() + { + "Accessibility.dll", + "Microsoft.VisualBasic.dll", + "Microsoft.VisualBasic.Forms.dll", + "System.Design.dll", + "System.Drawing.Common.dll", + "System.Drawing.Design.dll", + "System.Drawing.dll", + "System.Private.Windows.Core.dll", + "System.Windows.Forms.Design.dll", + "System.Windows.Forms.Design.Editors.dll", + "System.Windows.Forms.dll", + "System.Windows.Forms.Primitives.dll" + }; + + /// + /// Modifies each entry in the s_winFormsAssemblies list to include the provided folder name. + /// + /// The folder name to include in each entry. + public static ImmutableArray GetWinFormsBuildAssemblies(string tfm, string profile) + { + var winFormsAssembliesBuilder = + ImmutableArray.CreateBuilder(s_winFormsAssemblies.Count); + + string fullAssemblyPath = Path.Join( + WinFormsReferencesFactory.RepoRootPath, + "artifacts\\bin"); + + // Check, if s_winFormsAssemblies has all the assemblies in the folder: + foreach (string file in FindAssembliesInSubfolders(fullAssemblyPath, tfm, profile, s_winFormsAssemblies)) + { + winFormsAssembliesBuilder.Add(file); + } + + return winFormsAssembliesBuilder.ToImmutable(); + } + + private static ImmutableArray FindAssembliesInSubfolders( + string root, string tfm, string profile, IEnumerable assemblies) + { + var foundAssembliesBuilder = ImmutableArray.CreateBuilder(); + + foreach (string assembly in assemblies) + { + // Extract the base assembly folder name (without .dll extension) + string assemblyName = Path.GetFileNameWithoutExtension(assembly); + + // Get all directories at the root level + string[] possibleAssemblyFolders = Directory.GetDirectories(root); + + foreach (string assemblyFolder in possibleAssemblyFolders) + { + // Check if the expected profile and tfm subdirectory path exists + string pathToCheck = Path.Combine(assemblyFolder, profile, tfm); + + if (!Directory.Exists(pathToCheck)) + { + continue; + } + + // Look for the matching assembly file in this directory + string assemblyPath = Path.Combine(pathToCheck, assembly); + + if (File.Exists(assemblyPath)) + { + foundAssembliesBuilder.Add(assemblyPath); + break; + } + } + } + + return foundAssembliesBuilder.ToImmutable(); + } + /// /// Gets a reference assembly configuration for a specified .NET version. /// @@ -52,19 +134,71 @@ internal static class ReferenceAssemblyGenerator /// public static ReferenceAssemblies GetForLatestTFM(NetVersion version) { - if (s_exactNetVersionLookup.TryGetValue(version, out (string tfm, string exactVersion) value)) + if (version.HasFlag(NetVersion.WinFormsBuild)) + { + ReferenceAssemblies netRequestedTFMAssemblies = + WinFormsReferencesFactory.NetCoreAppReferences + ?? throw new NotSupportedException( + "The reference assemblies for the .NET Version based on the " + + "latest WinForms build couldn't be retrieved."); + + ReferenceAssemblies netCoreAppReferences = WinFormsReferencesFactory.NetCoreAppReferences + ?? throw new NotSupportedException( + "The AppCore reference assemblies for the .NET Version based on the " + + "latest WinForms build couldn't be retrieved."); + +#if DEBUG + string profile = "debug"; +#else + string profile = "release"; +#endif + + netRequestedTFMAssemblies = + netRequestedTFMAssemblies.AddAssemblies( + GetWinFormsBuildAssemblies( + tfm: netRequestedTFMAssemblies.TargetFramework, + profile: profile)); + + return netRequestedTFMAssemblies; + } + + (string tfm, string exactVersion) value; + + if (version.HasFlag(NetVersion.BuildOutput)) + { + version = version & ~NetVersion.BuildOutput; + + if (s_exactNetVersionLookup.TryGetValue(version, out value)) + { + var netRuntimePackage = new PackageIdentity(NetRuntimeIdentity, value.exactVersion); + + ReferenceAssemblies netRequestedTFMAssemblies = new ReferenceAssemblies( + targetFramework: value.tfm, + referenceAssemblyPackage: netRuntimePackage, + referenceAssemblyPath: $"ref\\{value.tfm}"); + + ReferenceAssemblies netCoreAppReferences = WinFormsReferencesFactory.NetCoreAppReferences + ?? throw new NotSupportedException( + "The AppCore reference assemblies for the .NET Version based on the " + + "latest WinForms build couldn't be retrieved."); + + return netRequestedTFMAssemblies; + } + } + + if (s_exactNetVersionLookup.TryGetValue(version, out value)) { var netRuntimePackage = new PackageIdentity(NetRuntimeIdentity, value.exactVersion); var netDesktopPackage = new PackageIdentity(NetDesktopIdentity, value.exactVersion); ReferenceAssemblies netRequestedTFMAssemblies = new ReferenceAssemblies( - value.tfm, - netRuntimePackage, - $"ref\\{value.tfm}"); + targetFramework: value.tfm, + referenceAssemblyPackage: netRuntimePackage, + referenceAssemblyPath: $"ref\\{value.tfm}"); netRequestedTFMAssemblies = netRequestedTFMAssemblies.WithPackages([netDesktopPackage]); - netRequestedTFMAssemblies.ResolveAsync(string.Empty,CancellationToken.None).Wait(); + netRequestedTFMAssemblies.ResolveAsync(string.Empty, CancellationToken.None).Wait(); return netRequestedTFMAssemblies; } diff --git a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/RoslynAnalyzerAndCodeFixTestBase.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/RoslynAnalyzerAndCodeFixTestBase.cs index 9f4ecb714e4..712feb6d666 100644 --- a/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/RoslynAnalyzerAndCodeFixTestBase.cs +++ b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/RoslynAnalyzerAndCodeFixTestBase.cs @@ -297,7 +297,15 @@ CSharpAnalyzerTest context OutputKind = OutputKind.WindowsApplication, }, ReferenceAssemblies = referenceAssemblies - }; + }; + + if (referenceAssemblies.Assemblies.Length > 0) + { + foreach (var assembly in referenceAssemblies.Assemblies) + { + context.TestState.AdditionalReferences.Add(assembly); + } + } if (globalUsing is not null) { @@ -376,6 +384,14 @@ protected CSharpCodeFixTest GetCodeFixTestContex NumberOfFixAllInDocumentIterations = numberOfFixAllIterations }; + if (referenceAssemblies.Assemblies.Length > 0) + { + foreach (var assembly in referenceAssemblies.Assemblies) + { + context.TestState.AdditionalReferences.Add(assembly); + } + } + // Include global using directives in both TestState and FixedState. if (fileSet.GlobalUsing is not null) { diff --git a/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.cs b/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.cs index 5f626089e69..28c37d52c6a 100644 --- a/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.cs +++ b/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.cs @@ -30,17 +30,25 @@ public override void Initialize(AnalysisContext context) private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { var invocationExpr = (InvocationExpressionSyntax)context.Node; + IMethodSymbol? methodSymbol = null; - if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr - || context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol is not IMethodSymbol methodSymbol - || methodSymbol.Name != InvokeAsyncString || methodSymbol.Parameters.Length != 2) + // Handle both explicit member access (this.InvokeAsync) and implicit method calls (InvokeAsync) + if (invocationExpr.Expression is MemberAccessExpressionSyntax memberAccessExpr) + { + methodSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol; + } + else if (invocationExpr.Expression is IdentifierNameSyntax identifierNameSyntax) + { + methodSymbol = context.SemanticModel.GetSymbolInfo(identifierNameSyntax).Symbol as IMethodSymbol; + } + + if (methodSymbol is null || methodSymbol.Name != InvokeAsyncString || methodSymbol.Parameters.Length != 2) { return; } - // Get the symbol of the method's instance: - TypeInfo objectTypeInfo = context.SemanticModel.GetTypeInfo(memberAccessExpr.Expression); IParameterSymbol funcParameter = methodSymbol.Parameters[0]; + INamedTypeSymbol? containingType = methodSymbol.ContainingType; // If the function delegate has a parameter (which makes then 2 type arguments), // we can safely assume it's a CancellationToken, otherwise the compiler would have @@ -54,11 +62,23 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) } // Let's make absolute clear, we're dealing with InvokeAsync of Control. - // (Not merging If statements to make it easier to read.) - if (objectTypeInfo.Type is not INamedTypeSymbol objectType - || !IsAncestorOrSelfOfType(objectType, "System.Windows.Forms.Control")) + // For implicit calls, we check the containing type of the method itself. + if (containingType is null || !IsAncestorOrSelfOfType(containingType, "System.Windows.Forms.Control")) { - return; + // For explicit calls, we need to check the instance type (from before) + if (invocationExpr.Expression is MemberAccessExpressionSyntax memberAccess) + { + TypeInfo objectTypeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression); + if (objectTypeInfo.Type is not INamedTypeSymbol objectType + || !IsAncestorOrSelfOfType(objectType, "System.Windows.Forms.Control")) + { + return; + } + } + else + { + return; + } } // And finally, let's check if the return type is Task or ValueTask, because those diff --git a/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationAnalyzer.cs b/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationAnalyzer.cs index 45eb85269cf..181d2d9ed67 100644 --- a/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationAnalyzer.cs +++ b/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationAnalyzer.cs @@ -24,26 +24,52 @@ public override void Initialize(AnalysisContext context) private static void AnalyzeSymbol(SymbolAnalysisContext context) { - // We analyze only properties. - var propertySymbol = (IPropertySymbol)context.Symbol; + // We only care about properties, and don't care about static properties. + if (context.Symbol is not IPropertySymbol propertySymbol + || propertySymbol.IsStatic) + { + return; + } + + // A property of System.ComponentModel.ISite we never flag. + if (propertySymbol.Type.Name == nameof(ISite) + && propertySymbol.Type.ContainingNamespace.ToString() == "System.ComponentModel") + { + return; + } - // Does the property belong to a class which derives from Component? + // If the property is part of any interface named IComponent, we're out. + if (propertySymbol.ContainingType.Name == nameof(IComponent)) + { + return; + } + + // Does the property belong to a class which implements the System.ComponentModel.IComponent interface? if (propertySymbol.ContainingType is null || !propertySymbol .ContainingType .AllInterfaces - .Any(i => i.Name == nameof(IComponent))) + .Any(i => i.Name == nameof(IComponent) && + i.ContainingNamespace is not null && + i.ContainingNamespace.ToString() == "System.ComponentModel")) { return; } - // Is the property read/write and at least internal? - if (propertySymbol.SetMethod is null + // Is the property read/write and at least internal and doesn't have a private setter? + if (propertySymbol.SetMethod is not IMethodSymbol propertySetter + || propertySetter.DeclaredAccessibility == Accessibility.Private || propertySymbol.DeclaredAccessibility < Accessibility.Internal) { return; } + // Skip overridden properties since the base property should already have the appropriate serialization configuration + if (propertySymbol.IsOverride) + { + return; + } + // Is the property attributed with DesignerSerializationVisibility or DefaultValue? if (propertySymbol.GetAttributes() .Any(a => a?.AttributeClass?.Name is (nameof(DesignerSerializationVisibilityAttribute)) diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs new file mode 100644 index 00000000000..108de1bb566 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.Analyzers.Diagnostics; +using System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms; +using System.Windows.Forms.CSharp.Analyzers.AvoidPassingTaskWithoutCancellationToken; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.WinForms.Test; +using Microsoft.WinForms.Utilities.Shared; + +namespace System.Windows.Forms.Analyzers.CSharp.Tests.AnalyzerTests.AvoidPassingTaskWithoutCancellationToken; + +/// +/// Tests for the AvoidPassingTaskWithoutCancellationTokenAnalyzer that verify it correctly +/// detects InvokeAsync calls without explicit 'this' keyword. +/// +public class ImplicitInvokeAsyncOnControl + : RoslynAnalyzerAndCodeFixTestBase +{ + /// + /// Initializes a new instance of the class. + /// + public ImplicitInvokeAsyncOnControl() + : base(SourceLanguage.CSharp) { } + + /// + /// Retrieves reference assemblies for the latest target framework versions. + /// + public static IEnumerable GetReferenceAssemblies() + { + NetVersion[] tfms = + [ + NetVersion.WinFormsBuild + ]; + + foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) + { + yield return new object[] { refAssembly }; + } + } + + /// + /// Tests that the analyzer detects InvokeAsync calls with Task return types + /// even when the 'this' keyword is omitted. + /// + [Theory] + [CodeTestData(nameof(GetReferenceAssemblies))] + public async Task DetectImplicitInvokeAsyncCalls( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) + { + // Make sure, we can resolve the assembly we're testing against: + var referenceAssembly = await referenceAssemblies.ResolveAsync( + language: string.Empty, + cancellationToken: CancellationToken.None); + + string diagnosticId = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken; + + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + + // Explicitly specify where diagnostics are expected + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(16, 15, 16, 61)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(17, 15, 17, 74)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(30, 19, 30, 65)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(64, 15, 64, 60)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(70, 15, 70, 65)); + + await context.RunAsync(); + } +} diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.cs new file mode 100644 index 00000000000..cd035c7e6d8 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.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 System.Windows.Forms; +using System.Threading; +using System.Threading.Tasks; + +namespace TestNamespace; + +public class MyForm : Form +{ + internal async Task DoWorkWithoutThis() + { + // Make sure, both get flagged, because they would + // not be awaited internally and became a fire-and-forget. + await InvokeAsync(async () => await Task.Delay(100)); + await this.InvokeAsync(async () => await DoWorkInNestedContext()); + } + + private async Task DoWorkInNestedContext() + { + await LocalFunction(); + bool test = await InvokeAsync( + DoSomethingWithTokenAsync, + CancellationToken.None); + + async Task LocalFunction() + { + // Make sure we detect this inside of a nested local function. + await InvokeAsync(async () => await Task.Delay(100)); + } + } + + // Helper methods for the test cases + private async Task DoSomethingAsync(CancellationToken token) + { + await Task.Delay(42 + 73, token); + return true; + } + + private async ValueTask DoSomethingWithTokenAsync(CancellationToken token) + { + bool flag = await DoSomethingAsync(token); + var meaningOfLife = 21 + 21; + + return (meaningOfLife == await GetMeaningOfLifeAsync(token)) && flag; + } + + private async Task GetMeaningOfLifeAsync(CancellationToken token) + { + DerivedForm derivedForm = new(); + await derivedForm.DoWorkInDerivedClassAsync(); + + await Task.Delay(100, token); + return 42; + } +} + +// Testing in a derived class to ensure the analyzer works with inheritance +public class DerivedForm : Form +{ + internal async Task DoWorkInDerivedClassAsync() + { + await InvokeAsync(async () => await Task.Delay(99)); + + await InvokeAsync(ct => new ValueTask( + task: DoSomethingStringAsync(ct)), + cancellationToken: CancellationToken.None); + + await this.InvokeAsync(async () => await Task.Delay(99)); + } + + private async Task DoSomethingStringAsync(CancellationToken token) + { + await Task.Delay(100, token); + return "test"; + } +} diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.cs new file mode 100644 index 00000000000..c1030d704f6 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CSharpControls; + +using System.Windows.Forms; + +public static class Program +{ + public static void Main(string[] args) + { + var form = new TestNamespace.MyForm(); + Application.Run(form); + } +} diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/CustomControlScenarios.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/CustomControlScenarios.cs index 63f55038df4..543869cf041 100644 --- a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/CustomControlScenarios.cs +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/CustomControlScenarios.cs @@ -67,7 +67,7 @@ public static IEnumerable GetReferenceAssemblies() NetVersion.Net6_0, NetVersion.Net7_0, NetVersion.Net8_0, - NetVersion.Net9_0 + NetVersion.WinFormsBuild ]; foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.cs new file mode 100644 index 00000000000..ff47bfe8c86 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.Analyzers.Diagnostics; +using System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms; +using System.Windows.Forms.CSharp.Analyzers.MissingPropertySerializationConfiguration; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.WinForms.Test; +using Microsoft.WinForms.Utilities.Shared; + +namespace System.Windows.Forms.Analyzers.CSharp.Tests.AnalyzerTests.MissingPropertySerializationConfiguration; + +/// +/// Tests specific edge cases for the MissingPropertySerializationConfigurationAnalyzer: +/// - Static properties which should not get flagged +/// - Properties in classes implementing non-System.ComponentModel.IComponent interfaces +/// - Properties with private setters +/// - Inherited properties that are already attributed correctly +/// +public class EdgeCaseScenarios + : RoslynAnalyzerAndCodeFixTestBase +{ + /// + /// Initializes a new instance of the class. + /// + public EdgeCaseScenarios() + : base(SourceLanguage.CSharp) + { + } + + /// + /// Retrieves reference assemblies for the latest target framework versions. + /// + public static IEnumerable GetReferenceAssemblies() + { + NetVersion[] tfms = + [ + NetVersion.Net6_0, + NetVersion.Net7_0, + NetVersion.Net8_0, + + // In this case, we're saying, we want to use .NET 9, but instead of using + // the Desktop-Package which comes with it, we take the BuildOutput from the + // this repo's artifacts folder. + NetVersion.WinFormsBuild + ]; + + foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) + { + yield return new object[] { refAssembly }; + } + } + + /// + /// Tests that the analyzer correctly handles edge cases: + /// - Not flagging static properties + /// - Not flagging properties in classes that implement non-System.ComponentModel.IComponent + /// - Not flagging properties with private setters + /// - Not flagging overridden properties when the base is properly attributed + /// + [Theory] + [CodeTestData(nameof(GetReferenceAssemblies))] + public async Task TestAnalyzerDiagnostics( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) + { + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + + string diagnosticId = DiagnosticIDs.MissingPropertySerializationConfiguration; + + // We expect no diagnostics for these edge cases + context.ExpectedDiagnostics.Clear(); + + context.ExpectedDiagnostics.Add( + DiagnosticResult + .CompilerError(diagnosticId) + .WithSpan(87, 23, 87, 38)); + + await context.RunAsync(); + } +} diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.cs new file mode 100644 index 00000000000..593096dba37 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.cs @@ -0,0 +1,130 @@ +// 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.ComponentModel; + +namespace Test +{ + // Custom IComponent interface in a different namespace + // This should not be detected by the analyzer + namespace CustomComponents + { + public interface IComponent : IDisposable + { + ISite Site { get; set; } + event EventHandler Disposed; + } + + public interface ISite : IServiceProvider + { + IComponent Component { get; } + IContainer Container { get; } + bool DesignMode { get; } + string Name { get; set; } + } + + public interface IContainer : IDisposable + { + ComponentCollection Components { get; } + void Add(IComponent component); + void Add(IComponent component, string name); + void Remove(IComponent component); + } + + public class ComponentCollection + { + // Implementation omitted + } + + // Component implementing the custom IComponent + // Properties here should not be flagged + public class CustomComponent : CustomComponents.IComponent + { + private ISite _site; + + public ISite Site + { + get { return _site; } + set { _site = value; } + } + + // This should not be flagged because it's from a custom IComponent + public string CustomProperty { get; set; } + + public event EventHandler Disposed; + + public void Dispose() + { + Disposed?.Invoke(this, EventArgs.Empty); + } + } + } + + // Component implementing System.ComponentModel.IComponent + public class MyComponent : System.ComponentModel.IComponent + { + private System.ComponentModel.ISite _site; + + public System.ComponentModel.ISite Site + { + get { return _site; } + set { _site = value; } + } + + public event EventHandler Disposed; + + // This should not be flagged because it's static + public static string StaticProperty { get; set; } + + // This should not be flagged because it has a private setter + public string PrivateSetterProperty { get; private set; } + + // This should not be flagged because it's internal with a private setter + internal string InternalPrivateSetterProperty { get; private set; } + + // This WOULD be flagged in a normal scenario (public read/write property) + public string RegularProperty { get; set; } + + public void Dispose() + { + Disposed?.Invoke(this, EventArgs.Empty); + } + } + + // Base component with properly attributed properties + public class BaseComponent : System.ComponentModel.IComponent + { + private System.ComponentModel.ISite _site; + + public System.ComponentModel.ISite Site + { + get { return _site; } + set { _site = value; } + } + + public event EventHandler Disposed; + + // Properly attributed with DesignerSerializationVisibility + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual string AttributedProperty { get; set; } + + // Properly attributed with DefaultValue + [DefaultValue("Default")] + public virtual string DefaultValueProperty { get; set; } + + public void Dispose() + { + Disposed?.Invoke(this, EventArgs.Empty); + } + } + + // Derived component with overridden properties + public class DerivedComponent : BaseComponent + { + // These should not be flagged because they are overrides + // and the base property is already properly attributed + public override string AttributedProperty { get; set; } + public override string DefaultValueProperty { get; set; } + } +} diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/GlobalUsing.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/GlobalUsing.cs new file mode 100644 index 00000000000..4147f6b0471 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/GlobalUsing.cs @@ -0,0 +1,2 @@ +global using System.Drawing; +global using System.Windows.Forms; diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.cs new file mode 100644 index 00000000000..a22a6988ffe --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace test; + +public static class Program +{ + public static void Main(string[] args) + { + var component = new Test.MyComponent(); + } +} diff --git a/src/System.Windows.Forms.Analyzers/cs/tests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj b/src/System.Windows.Forms.Analyzers/cs/tests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj index 7fa72c9072d..dfc2ee09e8c 100644 --- a/src/System.Windows.Forms.Analyzers/cs/tests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj +++ b/src/System.Windows.Forms.Analyzers/cs/tests/System.Windows.Forms.Analyzers.CSharp.Tests.csproj @@ -36,6 +36,8 @@ + + @@ -43,6 +45,9 @@ + + + @@ -60,25 +65,30 @@ + + - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never + + + AppConfigBuilder.cs diff --git a/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md b/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md new file mode 100644 index 00000000000..8f7a238d7b5 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md @@ -0,0 +1,600 @@ +# Writing Test Cases for WinForms Analyzers and CodeFixes + +## Purpose and Overview +This guide provides instructions for using AI to create comprehensive test cases for WinForms Analyzers and CodeFixes in both C# and Visual Basic. Following these guidelines will ensure consistent, maintainable, and effective tests across the codebase. + +## Table of Contents +1. [Project Structure](#project-structure) +2. [Language-Specific Considerations](#language-specific-considerations) +3. [Test Creation Workflow](#test-creation-workflow) +4. [Required Files](#required-files) +5. [Test File Structure](#test-file-structure) +6. [Test Implementation](#test-implementation) +7. [Code Examples](#code-examples) +8. [Troubleshooting](#troubleshooting) +9. [Quality Checklist](#quality-checklist) + +## Project Structure +We currently have 3 different test projects in the solution: +* `System.Windows.Forms.Analyzers.CSharp.Tests` +* `System.Windows.Forms.Analyzers.VisualBasic.Tests` +* `System.Windows.Forms.Analyzers.Tests` + +**Important**: AI assistance is currently only used for adding tests to the CSharp and VisualBasic test projects. + +## Language-Specific Considerations + +### C# Tests +* Written in C# and target analyzers written in C# +* Use the following infrastructure: + - Namespaces from `Microsoft.CodeAnalysis...` + - Namespace `Microsoft.CodeAnalysis.CSharp.Testing` + - Base class `RoslynAnalyzerAndCodeFixTestBase` +* Use `GetAnalyzerTestContext` and `GetCodeFixTestContext` methods +* Must include a `GlobalUsings.cs` file that includes at least `System.Windows.Forms` and `System.Drawing` + +### Visual Basic Tests +* Written in Visual Basic and target analyzers written in Visual Basic +* Use the following infrastructure: + - Namespaces from `Microsoft.CodeAnalysis...` + - Namespace `Microsoft.CodeAnalysis.VisualBasic.Testing` + - Base class `RoslynAnalyzerAndCodeFixTestBase(Of TAnalyzer, DefaultVerifier)` + - Extension class `VisualBasicAnalyzerAndCodeFixExtensions` for VB-specific requirements +* Use `GetVisualBasicAnalyzerTestContext` and `GetVisualBasicCodeFixTestContext` methods + +**Note**: Always specify whether you need C# or Visual Basic tests. Requests for generic "Analyzer tests" without specifying the language will be refused. + +**Important**: Clearly distinguish whether you need only Analyzer tests or both Analyzer and CodeFix tests. Only create the necessary test files based on this distinction. + +## Test Creation Workflow + +1. **Identify the target language** (C# or Visual Basic) +2. **Determine test scope** (Analyzer-only or Analyzer with CodeFix) +3. **Create the appropriate folder structure**: + ``` + Analyzer\[AnalyzerName]\TestData\ + ``` + Example: `Analyzer\EnsureModelDialogDisposed\TestData\` + +4. **Create a new test class** with a descriptive name + * For C#: Derive from `RoslynAnalyzerAndCodeFixTestBase` + * For VB: Derive from `RoslynAnalyzerAndCodeFixTestBase(Of TAnalyzer, DefaultVerifier)` + +5. **Create required files** (see [Required Files](#required-files)) + +6. **Create test data files**: + * Create a subfolder named the same as your test class + * Add the necessary test files based on test scope (see [Test File Structure](#test-file-structure)) + * Set BuildAction to `None` and "Copy to output directory" to `Do not copy` + +7. **Implement test methods** appropriate for the test scope +8. **Run and validate** the tests + +## Required Files + +### For C# Tests +1. **GlobalUsings.cs** - Must include at minimum: + ```csharp + global using System.Windows.Forms; + global using System.Drawing; + ``` + +2. **Program.cs** - A starting point that should ideally use some of the test class objects: + ```csharp + namespace MyNamespace; + + internal static class Program + { + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + // Optionally use test class objects here + Application.Run(new Form1()); + } + } + ``` + +### For Visual Basic Tests +* No equivalent to `GlobalUsings.cs` exists in Visual Basic, so, `Imports` need to be + added directly in the test files. +* We also need a `Program.vb` file that serves as the entry point for the application. + It can be looking like this, for the cases, where we need to setup +* Please note and take into account, that Visual Basic does not support local functions. + +```VB +Imports System +Imports System.Windows.Forms + +Namespace MyApplication + Friend NotInheritable Class Program + ''' + ''' The main entry point for the application. + ''' + _ + Shared Sub Main() + Application.EnableVisualStyles() + Application.SetCompatibleTextRenderingDefault(False) + Application.SetHighDpiMode(HighDpiMode.SystemAware) + Application.Run(New Form1()) + End Sub + End Class +End Namespace +``` + +## Test File Structure + +### For Analyzer-Only Tests +- **AnalyzerTestCode.cs/.vb**: + * Contains code that should trigger the analyzer or edge cases where it shouldn't trigger + * Used to verify the analyzer produces correct diagnostics +- **Additional supporting files** as needed (with clear naming conventions) + +### For CodeFix Tests +- **CodeFixTestCode.cs/.vb**: + * Contains code with marked regions that should be fixed by the CodeFixProvider + * Uses special markers `[|` and `|]` to highlight the exact code segments that should be fixed + * Example: `public SizeF [|ScaledSize|] { get; set; }` + +- **FixedTestCode.cs/.vb**: + * Contains the expected code after the CodeFixProvider has been applied + * Used to verify the CodeFix correctly transforms the code + +### Additional Files +- The test data folder can contain additional supporting files if needed for the test scenario +- All supporting files should follow consistent naming conventions and be clearly documented + +## Test Implementation + +### Analyzer-Only Test Method +For testing just the analyzer functionality without code fixes, you must explicitly specify where diagnostics are expected using `ExpectedDiagnostics.Add()`: + +```csharp +[Theory] +[CodeTestData(nameof(GetReferenceAssemblies))] +public async Task TestAnalyzerDiagnostics( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) +{ + // Make sure we can resolve the assembly we're testing against + var referenceAssembly = await referenceAssemblies.ResolveAsync( + language: string.Empty, + cancellationToken: CancellationToken.None); + + string diagnosticId = DiagnosticIDs.YourDiagnosticRuleId; + + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + + // Explicitly specify where diagnostics are expected + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(41, 21, 41, 97)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(44, 21, 44, 97)); + + await context.RunAsync(); +} +``` + +### Full CodeFix Test Methods +When testing both the analyzer and a code fix: + +```csharp +[Theory] +[CodeTestData(nameof(GetReferenceAssemblies))] +public async Task TestDiagnostics( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) +{ + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + await context.RunAsync(); + + context = GetFixedTestContext(fileSet, referenceAssemblies); + await context.RunAsync(); +} + +[Theory] +[CodeTestData(nameof(GetReferenceAssemblies))] +public async Task TestCodeFix( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) +{ + var context = GetCodeFixTestContext( + fileSet, + referenceAssemblies, + numberOfFixAllIterations: -2); + + context.CodeFixTestBehaviors = + CodeFixTestBehaviors.SkipFixAllInProjectCheck | + CodeFixTestBehaviors.SkipFixAllInSolutionCheck; + + await context.RunAsync(); +} +``` + +### Reference Assemblies Provider +Each test class should include a method to provide reference assemblies: + +```csharp +public static IEnumerable GetReferenceAssemblies() +{ + NetVersion[] tfms = + [ + NetVersion.Net6_0, + NetVersion.Net7_0, + NetVersion.Net8_0, + NetVersion.Net9_0 + ]; + + foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) + { + yield return new object[] { refAssembly }; + } +} +``` + +## Code Examples + +### GlobalUsings.cs Example for C# + +```csharp +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Windows.Forms; +global using System.Drawing; +global using Microsoft.CodeAnalysis; +global using Xunit; +``` + +### Program.cs Example for C# + +```csharp +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace TestNamespace; + +internal static class Program +{ + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + + // Optional: Use test class objects + var testForm = new TestForm(); + + Application.Run(new Form1()); + } +} +``` + +### C# Analyzer-Only Test Class Example + +```csharp +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms; +using System.Windows.Forms.CSharp.Analyzers.MyCustomAnalyzer; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.WinForms.Test; +using Microsoft.WinForms.Utilities.Shared; + +namespace System.Windows.Forms.Analyzers.CSharp.Tests.AnalyzerTests.MyCustomAnalyzer; + +/// +/// Tests for the MyCustomAnalyzer analyzer. +/// +public class MyCustomAnalyzerTests + : RoslynAnalyzerAndCodeFixTestBase +{ + /// + /// Initializes a new instance of the class. + /// + public MyCustomAnalyzerTests() + : base(SourceLanguage.CSharp) + { + } + + /// + /// Retrieves reference assemblies for the latest target framework versions. + /// + public static IEnumerable GetReferenceAssemblies() + { + NetVersion[] tfms = + [ + NetVersion.Net6_0, + NetVersion.Net7_0, + NetVersion.Net8_0, + NetVersion.Net9_0 + ]; + + foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) + { + yield return new object[] { refAssembly }; + } + } + + /// + /// Tests the diagnostics produced by the analyzer. + /// + [Theory] + [CodeTestData(nameof(GetReferenceAssemblies))] + public async Task TestDiagnostics( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) + { + // Make sure we can resolve the assembly we're testing against + var referenceAssembly = await referenceAssemblies.ResolveAsync( + language: string.Empty, + cancellationToken: CancellationToken.None); + + string diagnosticId = DiagnosticIDs.MyCustomAnalyzerRuleId; + + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + + // Explicitly specify where diagnostics are expected + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(10, 15, 10, 25)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(20, 10, 20, 35)); + + await context.RunAsync(); + } +} +``` + +### C# Complete Analyzer and CodeFix Test Class Example + +```csharp +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms; +using System.Windows.Forms.CSharp.Analyzers.MissingPropertySerializationConfiguration; +using System.Windows.Forms.CSharp.CodeFixes.AddDesignerSerializationVisibility; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.WinForms.Test; +using Microsoft.WinForms.Utilities.Shared; + +namespace System.Windows.Forms.Analyzers.CSharp.Tests.AnalyzerTests.MissingPropertySerializationConfiguration; + +/// +/// Represents a set of test scenarios for custom controls to verify +/// property serialization behavior. +/// +public class CustomControlScenarios + : RoslynAnalyzerAndCodeFixTestBase +{ + /// + /// Initializes a new instance of the class. + /// + public CustomControlScenarios() + : base(SourceLanguage.CSharp) + { + } + + /// + /// Retrieves reference assemblies for the latest target framework versions. + /// + public static IEnumerable GetReferenceAssemblies() + { + NetVersion[] tfms = + [ + NetVersion.Net6_0, + NetVersion.Net7_0, + NetVersion.Net8_0, + NetVersion.Net9_0 + ]; + + foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) + { + yield return new object[] { refAssembly }; + } + } + + /// + /// Tests the diagnostics produced by + /// . + /// + [Theory] + [CodeTestData(nameof(GetReferenceAssemblies))] + public async Task TestDiagnostics( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) + { + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + await context.RunAsync(); + + context = GetFixedTestContext(fileSet, referenceAssemblies); + await context.RunAsync(); + } + + /// + /// Tests the code-fix provider to ensure it correctly applies designer serialization attributes. + /// + [Theory] + [CodeTestData(nameof(GetReferenceAssemblies))] + public async Task TestCodeFix( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) + { + var context = GetCodeFixTestContext( + fileSet, + referenceAssemblies, + numberOfFixAllIterations: -2); + + context.CodeFixTestBehaviors = + CodeFixTestBehaviors.SkipFixAllInProjectCheck | + CodeFixTestBehaviors.SkipFixAllInSolutionCheck; + + await context.RunAsync(); + } +} +``` + +### Visual Basic Analyzer-Only Test Class Example + +```vb +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Imports System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms +Imports System.Windows.Forms.VisualBasic.Analyzers.MyCustomAnalyzer +Imports Microsoft.CodeAnalysis.Testing +Imports Microsoft.WinForms.Test +Imports Microsoft.WinForms.Utilities.Shared +Imports Xunit + +Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.MyCustomAnalyzer + + ''' + ''' Tests for the MyCustomAnalyzer analyzer. + ''' + Public Class MyCustomAnalyzerTests + Inherits RoslynAnalyzerAndCodeFixTestBase(Of MyCustomAnalyzer, DefaultVerifier) + + ''' + ''' Initializes a new instance of the class. + ''' + Public Sub New() + MyBase.New(SourceLanguage.VisualBasic) + End Sub + + ''' + ''' Retrieves reference assemblies for the latest target framework versions. + ''' + Public Shared Iterator Function GetReferenceAssemblies() As IEnumerable(Of Object()) + Dim tfms As NetVersion() = { + NetVersion.Net6_0, + NetVersion.Net7_0, + NetVersion.Net8_0, + NetVersion.Net9_0 + } + + For Each refAssembly In ReferenceAssemblyGenerator.GetForLatestTFMs(tfms) + Yield New Object() {refAssembly} + Next + End Function + + ''' + ''' Tests the diagnostics produced by the analyzer. + ''' + + + Public Async Function TestDiagnostics( + referenceAssemblies As ReferenceAssemblies, + fileSet As TestDataFileSet) As Task + + ' Make sure we can resolve the assembly we're testing against + Dim referenceAssembly = Await referenceAssemblies.ResolveAsync( + language:=String.Empty, + cancellationToken:=CancellationToken.None) + + Dim diagnosticId As String = DiagnosticIDs.MyCustomAnalyzerRuleId + + Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) + + ' Explicitly specify where diagnostics are expected + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(10, 15, 10, 25)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(20, 10, 20, 35)) + + Await context.RunAsync() + End Function + End Class + +End Namespace +``` + +### Code Fix Markers Explanation + +The code fix test files use special markers to denote regions that should be modified by the code fix: + +```csharp +// Before code fix: +public SizeF [|ScaledSize|] { get; set; } + +// After code fix: +[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] +public SizeF ScaledSize { get; set; } +``` + +The `[|` and `|]` markers precisely identify the text that the analyzer should flag for a diagnostic and that the code fix should transform. When the test is run: + +1. The test framework removes these markers before passing the code to the analyzer +2. It uses the marker positions to verify that diagnostics are reported at exactly these locations +3. It then applies the code fix and compares the result to the expected fixed code + +## Troubleshooting + +### Common Issues + +1. **Incorrect diagnostic locations** + - Ensure spans in `WithSpan()` match the actual code position + - Check that the markers in code fix test files enclose the exact code that needs fixing + +2. **Missing references** + - If tests fail with missing types, add the required references to your test context: + ```csharp + context.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(YourType).Assembly.Location)); + ``` + +3. **Test failures in specific target frameworks** + - Target framework-specific behavior can be handled with conditional checks: + ```csharp + if (referenceAssemblies.ToString().Contains("net7.0")) + { + // Special handling for .NET 7 + } + ``` + +4. **File generation confusion** + - Only create CodeFixTestCode.cs and FixedTestCode.cs files when specifically implementing a CodeFix test + - For Analyzer-only tests, only create the AnalyzerTestCode.cs file + +5. **Missing expected diagnostics** + - For Analyzer-only tests, always explicitly specify where diagnostics are expected using `context.ExpectedDiagnostics.Add()` + - The span coordinates (line, column, length) must precisely match where the diagnostic occurs in the code + +### Tips for Debugging + +- Use `context.ExpectedDiagnostics.Clear()` to handle cases where you want to test that no diagnostics are reported +- For complex code fix scenarios, consider breaking down the tests into smaller, focused test cases +- When diagnosing issues, temporarily add comments to your test files to mark important line numbers + +## Naming Conventions + +1. **Test Class Names** + - Use descriptive names that clearly identify the analyzer being tested + - End class names with "Tests" or "Scenarios" + - Examples: `EnsureModalDialogDisposedTests`, `CustomControlScenarios` + +2. **Test Method Names** + - Methods should describe what they're testing + - Use prefix "Test" for clarity + - Examples: `TestDiagnostics`, `TestCodeFix`, `TestEdgeCases` + +3. **Test File Names** + - Follow the established pattern: `AnalyzerTestCode.cs`, `CodeFixTestCode.cs`, `FixedTestCode.cs` + - For multiple scenarios, append a descriptive suffix: `AnalyzerTestCode_UserControl.cs` + - Supporting files should have clear, descriptive names related to their purpose + +## Quality Checklist + +Before submitting your tests, verify the following: + +- [ ] Test class inherits from the correct base class for the target language +- [ ] Test class uses the correct language in constructor (C# or VB) +- [ ] GlobalUsings.cs is included for C# tests with required imports +- [ ] Program.cs is included as a starting point +- [ ] Test files have correct BuildAction (None) and Copy settings +- [ ] Folder structure follows conventions +- [ ] All necessary usings/imports are included +- [ ] Tests cover both positive cases (diagnostic should trigger) and negative cases (diagnostic should not trigger) +- [ ] For Analyzer-only tests, explicit ExpectedDiagnostics are specified with exact locations +- [ ] Only appropriate test files are created (Analyzer-only vs. CodeFix scenarios) +- [ ] Code fix tests verify the correct transformation of code (when applicable) +- [ ] Tests include proper documentation and summaries +- [ ] Tests run successfully against all target frameworks +- [ ] Markers in code fix test files correctly identify the regions to be fixed (when applicable) diff --git a/src/System.Windows.Forms.Analyzers/prompting/SamplePrompt for AnalyzerTests.md b/src/System.Windows.Forms.Analyzers/prompting/SamplePrompt for AnalyzerTests.md new file mode 100644 index 00000000000..96ba2f2645f --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/prompting/SamplePrompt for AnalyzerTests.md @@ -0,0 +1,17 @@ +Can you please implement an additional Analyzer unit test according to +#file:'AnalyzerTests-Copilot-Instructions.md' for the +#class:'System.Windows.Forms.CSharp.Analyzers.MissingPropertySerializationConfiguration.MissingPropertySerializationConfigurationAnalyzer':436-4056 ? + +We need to a test class which tests that makes sure +* No static Properties get flagged. +* No properties get flagged in side of classes which are inherited/implemented based of + `IComponent` alright, but not the `System.ComponentModel` versions. +* No Properties with a private setting get flagged. +* We have at least one test case, where we have an inherited property which has + been correctly attributed, so, the overwritten one should or should not cause + the Analyzer to trigger. + +These four cases can be combined using one additional test class, and one additional +test data folder. + +This is for C#. diff --git a/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.vb b/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.vb index ff6a30f001e..c8f530ced57 100644 --- a/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.vb +++ b/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/AvoidPassingTaskWithoutCancellationToken/AvoidPassingTaskWithoutCancellationTokenAnalyzer.vb @@ -31,21 +31,23 @@ Namespace Global.System.Windows.Forms.VisualBasic.Analyzers.AvoidPassingTaskWith Private Sub AnalyzeInvocation(context As SyntaxNodeAnalysisContext) Dim invocationExpr = DirectCast(context.Node, InvocationExpressionSyntax) - - If Not (TypeOf invocationExpr.Expression Is MemberAccessExpressionSyntax) Then - Return + Dim methodSymbol As IMethodSymbol = Nothing + + ' Handle both explicit member access (Me.InvokeAsync) and implicit method calls (InvokeAsync) + If TypeOf invocationExpr.Expression Is MemberAccessExpressionSyntax Then + Dim memberAccessExpr = DirectCast(invocationExpr.Expression, MemberAccessExpressionSyntax) + methodSymbol = TryCast(context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol, IMethodSymbol) + ElseIf TypeOf invocationExpr.Expression Is IdentifierNameSyntax Then + Dim identifierNameSyntax = DirectCast(invocationExpr.Expression, IdentifierNameSyntax) + methodSymbol = TryCast(context.SemanticModel.GetSymbolInfo(identifierNameSyntax).Symbol, IMethodSymbol) End If - Dim memberAccessExpr = DirectCast(invocationExpr.Expression, MemberAccessExpressionSyntax) - Dim methodSymbol = TryCast(context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol, IMethodSymbol) - If methodSymbol Is Nothing OrElse methodSymbol.Name <> InvokeAsyncString OrElse methodSymbol.Parameters.Length <> 2 Then Return End If - ' Get the symbol of the method's instance: - Dim objectTypeInfo As TypeInfo = context.SemanticModel.GetTypeInfo(memberAccessExpr.Expression) Dim funcParameter As IParameterSymbol = methodSymbol.Parameters(0) + Dim containingType As INamedTypeSymbol = methodSymbol.ContainingType ' If the function delegate has a parameter (which makes then 2 type arguments), ' we can safely assume it's a CancellationToken, otherwise the compiler would have @@ -62,15 +64,25 @@ Namespace Global.System.Windows.Forms.VisualBasic.Analyzers.AvoidPassingTaskWith End If ' Let's make absolute clear, we're dealing with InvokeAsync of Control. - ' (Not merging If statements to make it easier to read.) - If Not (TypeOf objectTypeInfo.Type Is INamedTypeSymbol) Then - Return - End If - - Dim objectType = DirectCast(objectTypeInfo.Type, INamedTypeSymbol) - - If Not IsAncestorOrSelfOfType(objectType, "System.Windows.Forms.Control") Then - Return + ' For implicit calls, we check the containing type of the method itself. + If containingType Is Nothing OrElse Not IsAncestorOrSelfOfType(containingType, "System.Windows.Forms.Control") Then + ' For explicit calls, we need to check the instance type (from before) + If TypeOf invocationExpr.Expression Is MemberAccessExpressionSyntax Then + Dim memberAccess = DirectCast(invocationExpr.Expression, MemberAccessExpressionSyntax) + Dim objectTypeInfo As TypeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression) + + If Not (TypeOf objectTypeInfo.Type Is INamedTypeSymbol) Then + Return + End If + + Dim objectType = DirectCast(objectTypeInfo.Type, INamedTypeSymbol) + + If Not IsAncestorOrSelfOfType(objectType, "System.Windows.Forms.Control") Then + Return + End If + Else + Return + End If End If ' And finally, let's check if the return type is Task or ValueTask, because those @@ -91,8 +103,8 @@ Namespace Global.System.Windows.Forms.VisualBasic.Analyzers.AvoidPassingTaskWith ' Helper method to check if a type is of a certain type or a derived type. Private Shared Function IsAncestorOrSelfOfType(type As INamedTypeSymbol, typeName As String) As Boolean Return type IsNot Nothing AndAlso - type.ToString() = typeName OrElse - IsAncestorOrSelfOfType(type.BaseType, typeName) + (type.ToString() = typeName OrElse + IsAncestorOrSelfOfType(type.BaseType, typeName)) End Function End Class End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationDiagnosticAnalyzer.vb b/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationDiagnosticAnalyzer.vb index c5ff05bf0d2..616f822e6c8 100644 --- a/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationDiagnosticAnalyzer.vb +++ b/src/System.Windows.Forms.Analyzers/vb/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationDiagnosticAnalyzer.vb @@ -32,18 +32,40 @@ Namespace Global.System.Windows.Forms.VisualBasic.Analyzers.MissingPropertySeria Return End If - ' Does the property belong to a class which derives from Component? + ' A property of System.ComponentModel.ISite we never flag. + If propertySymbol.Type.Name = NameOf(ISite) AndAlso + propertySymbol.Type.ContainingNamespace.ToString() = "System.ComponentModel" Then + Return + End If + + ' If the property is part of any interface named IComponent, we're out. + If propertySymbol.ContainingType.Name = NameOf(IComponent) Then + Return + End If + + ' Does the property belong to a class which implements the System.ComponentModel.IComponent interface? If propertySymbol.ContainingType Is Nothing OrElse Not propertySymbol.ContainingType.AllInterfaces.Any( - Function(i) i.Name = NameOf(IComponent)) Then + Function(i) i.Name = NameOf(IComponent) AndAlso + i.ContainingNamespace IsNot Nothing AndAlso + i.ContainingNamespace.ToString() = "System.ComponentModel") Then + Return + End If + ' Skip static properties since they are not serialized by the designer + If propertySymbol.IsStatic Then Return End If - ' Is the property read/write and at least internal? + ' Is the property read/write, at least internal, and doesn't have a private setter? If propertySymbol.SetMethod Is Nothing OrElse + propertySymbol.SetMethod.DeclaredAccessibility = Accessibility.Private OrElse propertySymbol.DeclaredAccessibility < Accessibility.Internal Then + Return + End If + ' Skip overridden properties since the base property should already have the appropriate serialization configuration + If propertySymbol.IsOverride Then Return End If diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb new file mode 100644 index 00000000000..fa2a2fb0332 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb @@ -0,0 +1,71 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Imports System.Windows.Forms.Analyzers.Diagnostics +Imports System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms +Imports System.Windows.Forms.VisualBasic.Analyzers.AvoidPassingTaskWithoutCancellationToken +Imports Microsoft.CodeAnalysis.Testing +Imports Microsoft.WinForms.Test +Imports Microsoft.WinForms.Utilities.Shared +Imports Xunit + +Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.AvoidPassingTaskWithoutCancellationToken + + ''' + ''' Tests for the AvoidPassingTaskWithoutCancellationTokenAnalyzer that verify it correctly + ''' detects InvokeAsync calls without explicit 'Me' keyword. + ''' + Public Class ImplicitInvokeAsyncOnControl + Inherits RoslynAnalyzerAndCodeFixTestBase(Of AvoidPassingTaskWithoutCancellationTokenAnalyzer, DefaultVerifier) + + ''' + ''' Initializes a new instance of the class. + ''' + Public Sub New() + MyBase.New(SourceLanguage.VisualBasic) + End Sub + + ''' + ''' Retrieves reference assemblies for the latest target framework versions. + ''' + Public Shared Iterator Function GetReferenceAssemblies() As IEnumerable(Of Object()) + Dim tfms As NetVersion() = { + NetVersion.Net9_0 + } + + For Each refAssembly In ReferenceAssemblyGenerator.GetForLatestTFMs(tfms) + Yield New Object() {refAssembly} + Next + End Function + + ''' + ''' Tests that the analyzer detects InvokeAsync calls with Task return types + ''' even when the 'Me' keyword is omitted. + ''' + + + Public Async Function DetectImplicitInvokeAsyncCalls( + referenceAssemblies As ReferenceAssemblies, + fileSet As TestDataFileSet) As Task + + ' Make sure, we can resolve the assembly we're testing against: + Dim referenceAssembly = Await referenceAssemblies.ResolveAsync( + language:=String.Empty, + cancellationToken:=CancellationToken.None) + + Dim diagnosticId As String = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken + + Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) + + ' Explicitly specify where diagnostics are expected + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(18, 19, 20, 44)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(22, 19, 24, 47)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(41, 19, 43, 44)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(73, 19, 75, 44)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(82, 19, 84, 47)) + + Await context.RunAsync() + End Function + End Class + +End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/InvokeAsyncOnControl.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/InvokeAsyncOnControl.vb index e1f3664e650..9a9af9daf30 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/InvokeAsyncOnControl.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/InvokeAsyncOnControl.vb @@ -33,6 +33,7 @@ Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.AvoidPa Public Async Function AvoidPassingTaskWithoutCancellationAnalyzer( referenceAssemblies As ReferenceAssemblies, fileSet As TestDataFileSet) As Task + ' Make sure, we can resolve the assembly we're testing against: ' Always pass `String.Empty` for the language here to keep it generic. Dim referenceAssembly = Await referenceAssemblies.ResolveAsync( @@ -42,9 +43,9 @@ Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.AvoidPa Dim diagnosticId As String = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(37, 25, 37, 84)) - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(40, 25, 40, 84)) - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(43, 25, 43, 85)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(45, 25, 45, 84)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(48, 25, 48, 84)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(51, 25, 51, 85)) Await context.RunAsync() End Function diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.vb new file mode 100644 index 00000000000..a8b656774f7 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.vb @@ -0,0 +1,93 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Option Strict On +Option Explicit On + +Imports System.Windows.Forms +Imports System.Threading +Imports System.Threading.Tasks + +Namespace TestNamespace + Public Class MyForm + Inherits Form + + Friend Async Function DoWorkWithoutThis() As Task + ' Make sure, both get flagged, because they would + ' not be awaited internally and became a fire-and-forget. + Await InvokeAsync(Async Function() As Task + Await Task.Delay(100) + End Function) + + Await Me.InvokeAsync(Async Function() As Task + Await DoWorkInNestedContext() + End Function) + End Function + + Private Async Function DoWorkInNestedContext() As Task + + Await NestedFunction() + + Await InvokeAsync( + Function(ct) New ValueTask( + DoSomethingWithTokenAsync(ct)), + CancellationToken.None) + + End Function + + Private Async Function NestedFunction() As Task + + ' Make sure we detect this inside of a nested function. + Await InvokeAsync(Async Function() + Await Task.Delay(100) + End Function) + End Function + + ' Helper methods for the test cases + Private Async Function DoSomethingAsync(token As CancellationToken) As Task + Await Task.Delay(42 + 73, token) + End Function + + Private Async Function DoSomethingWithTokenAsync(token As CancellationToken) As Task(Of Boolean) + ' VB cannot await ValueTask directly, so convert to Task + Await DoSomethingAsync(token) + Dim meaningOfLife As Integer = 21 + 21 + + Return meaningOfLife = Await GetMeaningOfLifeAsync(token) + End Function + + Private Async Function GetMeaningOfLifeAsync(token As CancellationToken) As Task(Of Integer) + Dim derivedForm As New DerivedForm() + Await derivedForm.DoWorkInDerivedClassAsync() + + Await Task.Delay(100, token) + Return 42 + End Function + End Class + + ' Testing in a derived class to ensure the analyzer works with inheritance + Public Class DerivedForm + Inherits Form + + Friend Async Function DoWorkInDerivedClassAsync() As Task + Await InvokeAsync(Async Function() + Await Task.Delay(99) + End Function) + + ' ValueTask handling in VB needs conversion to Task + Await InvokeAsync(Function(ct) New ValueTask(Of String)( + DoSomethingStringAsync(ct)), + CancellationToken.None) + + Await Me.InvokeAsync(Async Function() + Await Task.Delay(99) + End Function) + End Function + + Private Async Function DoSomethingStringAsync(token As CancellationToken) As Task(Of String) + Await Task.Delay(100, token) + Return "test" + End Function + + End Class +End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.vb new file mode 100644 index 00000000000..c1b2796be96 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.vb @@ -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. + +Option Strict On +Option Explicit On + +Imports System +Imports System.Windows.Forms + +Namespace MyApplication + Friend NotInheritable Class Program + ''' + ''' The main entry point for the application. + ''' + + Shared Sub Main() + Application.EnableVisualStyles() + Application.SetCompatibleTextRenderingDefault(False) + Application.SetHighDpiMode(HighDpiMode.SystemAware) + Application.Run(New TestNamespace.MyForm()) + End Sub + End Class +End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/InvokeAsyncOnControl/AnalyzerTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/InvokeAsyncOnControl/AnalyzerTestCode.vb index 84043c9465c..812e605ef4f 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/InvokeAsyncOnControl/AnalyzerTestCode.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/InvokeAsyncOnControl/AnalyzerTestCode.vb @@ -1,4 +1,10 @@ -Imports System +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Option Strict On +Option Explicit On + +Imports System Imports System.Threading Imports System.Threading.Tasks Imports System.Windows.Forms @@ -15,17 +21,19 @@ Namespace VisualBasicControls ' A sync Func delegate is also fine. Dim okFunc As New Func(Of Integer)(Function() 42) - ' Just a Task we will get in trouble since it's handled as a fire and forget. - Dim notOkAsyncFunc As New Func(Of Task)(Function() - control.Text = "Hello, World!" - Return Task.CompletedTask - End Function) + ' Just a Task - we will get in trouble since it's handled as a fire and forget. + Dim notOkAsyncFunc As New Func(Of Task)( + Function() + control.Text = "Hello, World!" + Return Task.CompletedTask + End Function) ' A Task returning a value will also get us in trouble since it's handled as a fire and forget. - Dim notOkAsyncFunc2 As New Func(Of Task(Of Integer))(Function() - control.Text = "Hello, World!" - Return Task.FromResult(42) - End Function) + Dim notOkAsyncFunc2 As New Func(Of Task(Of Integer))( + Function() + control.Text = "Hello, World!" + Return Task.FromResult(42) + End Function) ' OK. Dim task1 = control.InvokeAsync(okAction) @@ -43,16 +51,18 @@ Namespace VisualBasicControls Dim task5 = control.InvokeAsync(notOkAsyncFunc2, CancellationToken.None) ' This is OK, since we're passing a cancellation token. - Dim okAsyncFunc As New Func(Of CancellationToken, ValueTask)(Function(cancellation) - control.Text = "Hello, World!" - Return ValueTask.CompletedTask - End Function) + Dim okAsyncFunc As New Func(Of CancellationToken, ValueTask)( + Function(cancellation) + control.Text = "Hello, World!" + Return ValueTask.CompletedTask + End Function) ' This is also OK, again, because we're passing a cancellation token. - Dim okAsyncFunc2 As New Func(Of CancellationToken, ValueTask(Of Integer))(Function(cancellation) - control.Text = "Hello, World!" - Return ValueTask.FromResult(42) - End Function) + Dim okAsyncFunc2 As New Func(Of CancellationToken, ValueTask(Of Integer))( + Function(cancellation) + control.Text = "Hello, World!" + Return ValueTask.FromResult(42) + End Function) ' And let's test that, too: Dim task6 = control.InvokeAsync(okAsyncFunc, CancellationToken.None) diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb new file mode 100644 index 00000000000..9f4908e0594 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb @@ -0,0 +1,74 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Imports System.Windows.Forms.Analyzers.Diagnostics +Imports System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms +Imports System.Windows.Forms.VisualBasic.Analyzers.MissingPropertySerializationConfiguration +Imports Microsoft.CodeAnalysis.Testing +Imports Microsoft.WinForms.Test +Imports Microsoft.WinForms.Utilities.Shared +Imports Xunit + +Namespace Global.System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.MissingPropertySerializationConfiguration + + ''' + ''' Tests specific edge cases for the MissingPropertySerializationConfigurationAnalyzer: + ''' - Static properties which should not get flagged + ''' - Properties in classes implementing non-System.ComponentModel.IComponent interfaces + ''' - Properties with private setters + ''' - Inherited properties that are already attributed correctly + ''' + Public Class EdgeCaseScenarios + Inherits RoslynAnalyzerAndCodeFixTestBase(Of MissingPropertySerializationConfigurationAnalyzer, DefaultVerifier) + + ''' + ''' Initializes a new instance of the class. + ''' + Public Sub New() + MyBase.New(SourceLanguage.VisualBasic) + End Sub + + ''' + ''' Retrieves reference assemblies for the latest target framework versions. + ''' + Public Shared Iterator Function GetReferenceAssemblies() As IEnumerable(Of Object()) + Dim tfms As NetVersion() = { + NetVersion.Net6_0, + NetVersion.Net7_0, + NetVersion.Net8_0, + NetVersion.WinFormsBuild ' Build from artifacts folder + } + + For Each refAssembly In ReferenceAssemblyGenerator.GetForLatestTFMs(tfms) + Yield New Object() {refAssembly} + Next + End Function + + ''' + ''' Tests that the analyzer correctly handles edge cases: + ''' - Not flagging static properties + ''' - Not flagging properties in classes that implement non-System.ComponentModel.IComponent + ''' - Not flagging properties with private setters + ''' - Not flagging overridden properties when the base is properly attributed + ''' + + + Public Async Function TestAnalyzerDiagnostics( + referenceAssemblies As ReferenceAssemblies, + fileSet As TestDataFileSet) As Task + + Dim diagnosticId = DiagnosticIDs.MissingPropertySerializationConfiguration + + Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) + + context.ExpectedDiagnostics.Clear() + + ' Only expect diagnostic on the one property that should be flagged + context.ExpectedDiagnostics.Add( + DiagnosticResult.CompilerError(diagnosticId).WithSpan(112, 25, 112, 40)) + + Await context.RunAsync() + End Function + End Class + +End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/AnalyzerTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/AnalyzerTestCode.vb index 3c6bbaf2c67..e4cf1922e68 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/AnalyzerTestCode.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/AnalyzerTestCode.vb @@ -1,4 +1,7 @@ -Imports System.ComponentModel +Option Strict On +Option Explicit On + +Imports System.ComponentModel Imports System.Drawing Namespace VisualBasicControls diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/CodeFixTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/CodeFixTestCode.vb index afae9ccfc9d..495ba07edcb 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/CodeFixTestCode.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/CodeFixTestCode.vb @@ -1,4 +1,7 @@ -Imports System.ComponentModel +Option Strict On +Option Explicit On + +Imports System.ComponentModel Imports System.Drawing Namespace VisualBasicControls diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/FixedTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/FixedTestCode.vb index 7a55cc6c2c3..57d51756d54 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/FixedTestCode.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/FixedTestCode.vb @@ -1,4 +1,7 @@ -Imports System.ComponentModel +Option Strict On +Option Explicit On + +Imports System.ComponentModel Imports System.Drawing Namespace VisualBasicControls diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/Program.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/Program.vb index 23fe8cb0163..ade8302a033 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/Program.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/CustomControlScenarios/Program.vb @@ -1,6 +1,9 @@ ' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. +Option Strict On +Option Explicit On + Imports System.Drawing Namespace VisualBasicControls diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.vb new file mode 100644 index 00000000000..2cde88f2b7c --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.vb @@ -0,0 +1,158 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +Option Strict On +Option Explicit On + +Imports System +Imports System.ComponentModel + +Namespace Test + + ' Custom IComponent interface in a different namespace + ' This should not be detected by the analyzer + Namespace CustomComponents + + Public Interface IComponent + Inherits IDisposable + + Property Site As ISite + Event Disposed As EventHandler + End Interface + + Public Interface ISite + Inherits IServiceProvider + + ReadOnly Property Component As IComponent + ReadOnly Property Container As IContainer + ReadOnly Property DesignMode As Boolean + Property Name As String + End Interface + + Public Interface IContainer + Inherits IDisposable + + ReadOnly Property Components As ComponentCollection + Sub Add(component As IComponent) + Sub Add(component As IComponent, name As String) + Sub Remove(component As IComponent) + End Interface + + Public Class ComponentCollection + ' Implementation omitted + End Class + + ' Component implementing the custom IComponent + ' Properties here should not be flagged + Public Class CustomComponent + Implements CustomComponents.IComponent + + Private _site As ISite + + Public Property Site As ISite Implements IComponent.Site + Get + Return _site + End Get + Set(value As ISite) + _site = value + End Set + End Property + + ' This should not be flagged because it's from a custom IComponent + Public Property CustomProperty As String + + Public Event Disposed As EventHandler Implements IComponent.Disposed + + Public Sub Dispose() Implements IDisposable.Dispose + RaiseEvent Disposed(Me, EventArgs.Empty) + End Sub + End Class + End Namespace + + ' Component implementing System.ComponentModel.IComponent + Public Class MyComponent + Implements System.ComponentModel.IComponent + + Private _site As System.ComponentModel.ISite + + Public Property Site As System.ComponentModel.ISite Implements System.ComponentModel.IComponent.Site + Get + Return _site + End Get + Set(value As System.ComponentModel.ISite) + _site = value + End Set + End Property + + Public Event Disposed As EventHandler Implements System.ComponentModel.IComponent.Disposed + + ' This should not be flagged because it's static + Public Shared Property StaticProperty As String + + ' This should not be flagged because it has a private setter + Public Property PrivateSetterProperty As String + Get + Return String.Empty + End Get + Private Set(value As String) + ' Do nothing + End Set + End Property + + ' This should not be flagged because it's internal with a private setter + Friend Property InternalPrivateSetterProperty As String + Get + Return String.Empty + End Get + Private Set(value As String) + ' Do nothing + End Set + End Property + + ' This WOULD be flagged in a normal scenario (public read/write property) + Public Property RegularProperty As String + + Public Sub Dispose() Implements IDisposable.Dispose + RaiseEvent Disposed(Me, EventArgs.Empty) + End Sub + End Class + + ' Base component with properly attributed properties + Public Class BaseComponent + Implements System.ComponentModel.IComponent + + Private _site As System.ComponentModel.ISite + + Public Property Site As System.ComponentModel.ISite Implements System.ComponentModel.IComponent.Site + Get + Return _site + End Get + Set(value As System.ComponentModel.ISite) + _site = value + End Set + End Property + + Public Event Disposed As EventHandler Implements System.ComponentModel.IComponent.Disposed + + ' Properly attributed with DesignerSerializationVisibility + + Public Overridable Property AttributedProperty As String + + ' Properly attributed with DefaultValue + + Public Overridable Property DefaultValueProperty As String + + Public Sub Dispose() Implements IDisposable.Dispose + RaiseEvent Disposed(Me, EventArgs.Empty) + End Sub + End Class + + ' Derived component with overridden properties + Public Class DerivedComponent + Inherits BaseComponent + + ' These should not be flagged because they are overrides + ' and the base property is already properly attributed + Public Overrides Property AttributedProperty As String + Public Overrides Property DefaultValueProperty As String + End Class +End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.vb new file mode 100644 index 00000000000..b561b457ae0 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.vb @@ -0,0 +1,19 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Option Strict On +Option Explicit On + +Imports System + +Namespace Test + Friend NotInheritable Class Program + ''' + ''' The main entry point for the application. + ''' + + Shared Sub Main() + Dim component As New MyComponent() + End Sub + End Class +End Namespace diff --git a/src/System.Windows.Forms.Analyzers/vb/tests/System.Windows.Forms.Analyzers.VisualBasic.Tests.vbproj b/src/System.Windows.Forms.Analyzers/vb/tests/System.Windows.Forms.Analyzers.VisualBasic.Tests.vbproj index c5690e75ea9..19729e3af28 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/System.Windows.Forms.Analyzers.VisualBasic.Tests.vbproj +++ b/src/System.Windows.Forms.Analyzers/vb/tests/System.Windows.Forms.Analyzers.VisualBasic.Tests.vbproj @@ -11,11 +11,16 @@ + + + + + @@ -50,11 +55,16 @@ + + + + +