From 79351106ea71fb72d3df22af99823d7c507d5993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Sun, 30 Mar 2025 17:19:01 -0700 Subject: [PATCH 01/11] Fix Analyzer issue, where we did not make sure to test for the _right_ IComponent. --- .../MissingPropertySerializationConfigurationAnalyzer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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..97b8885bc6d 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 @@ -27,12 +27,14 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) // We analyze only properties. var propertySymbol = (IPropertySymbol)context.Symbol; - // Does the property belong to a class which derives from Component? + // 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; } From d40fb65ca1efc5b3f62a6532ccb6629a76e0733b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Mon, 31 Mar 2025 09:16:35 -0700 Subject: [PATCH 02/11] Addressing the over-reporting issues in the MissingPropertySerializationConfiguration. * Addresses 13205, 13206, 13207, 13208. --- ...ingPropertySerializationConfigurationAnalyzer.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 97b8885bc6d..ec424a16424 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 @@ -39,6 +39,12 @@ i.ContainingNamespace is not null && return; } + // Skip static properties since they are not serialized by the designer + if (propertySymbol.IsStatic) + { + return; + } + // Is the property read/write and at least internal? if (propertySymbol.SetMethod is null || propertySymbol.DeclaredAccessibility < Accessibility.Internal) @@ -46,6 +52,13 @@ i.ContainingNamespace is not null && return; } + // Skip overridden properties since the base property should already have the appropriate serialization configuration + // TODO: Does just this cover all the cases, particularly for legacy code where the dev doesn't own the source? + if (propertySymbol.IsOverride) + { + return; + } + // Is the property attributed with DesignerSerializationVisibility or DefaultValue? if (propertySymbol.GetAttributes() .Any(a => a?.AttributeClass?.Name is (nameof(DesignerSerializationVisibilityAttribute)) From 365caa74762f6567da9ed2e3a1f3691c9aa0a987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Mon, 31 Mar 2025 12:49:21 -0700 Subject: [PATCH 03/11] Add specific Copilot instructions for writing Analyzer and CodeFix tests. --- AnalyzerTests-Copilot-Instructions.md | 340 ++++++++++++++++++ Winforms.sln | 3 + ...indows.Forms.Analyzers.CSharp.Tests.csproj | 12 +- 3 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 AnalyzerTests-Copilot-Instructions.md diff --git a/AnalyzerTests-Copilot-Instructions.md b/AnalyzerTests-Copilot-Instructions.md new file mode 100644 index 00000000000..2ec2584097c --- /dev/null +++ b/AnalyzerTests-Copilot-Instructions.md @@ -0,0 +1,340 @@ +# Copilot-Instructions to write test cases for WinForms Analyzers and CodeFixes + +For Analyzer tests, we have currently 3 different projects in the solution. +* System.Windows.Forms.Analyzers.CSharp.Tests +* System.Windows.Forms.Analyzers.VisualBasic.Tests +* System.Windows.Forms.Analyzers.Tests + +Important: +We are using AI currently only for adding tests to CSharp and VisualBasic tests projects. + +The approach is principally the same for both CSharp and VisualBasic. +But here are the important differences: + +* The CSharp tests are written in CSharp and are targeting the Analyzers which are also written in CSharp. +* When CSharp tests are using the test existing infrastructure, they are based on + - The namespaces around `Microsoft.CodeAnalysis...` + - The namespace `Microsoft.CodeAnalysis.CSharp.Testing` + - The test base class `RoslynAnalyzerAndCodeFixTestBase` + +* The VisualBasic tests are written in VisualBasic and are targeting the Analyzers which are also written in VisualBasic. +* When VisualBasic tests are using the test existing infrastructure, they are based on + - The namespaces around `Microsoft.CodeAnalysis...` + - The namespace `Microsoft.CodeAnalysis.VisualBasic.Testing` + - The test base class `RoslynAnalyzerAndCodeFixTestBase(Of TAnalyzer, DefaultVerifier)` + - But in addition the extension class `VisualBasicAnalyzerAndCodeFixExtensions`, so that the requirements for Visual Basic are met. + +* We always ask for CSharp and Visual Basic tests separately. If not specified and being ask + to write Analyzer tests, please refuse and ask to rephrase the request and specify the language. + +## General approach + +Inside of the respective test projects, a new folder for a new test series for an Analyzer should be +created in way, which is describing the Analyzer sufficiently, be under the folder `Analyzer`. Under that +folder, please create an additional folder `TestData` in addition. For example in _System.Windows.Forms.Analyzers.CSharp.Tests_ +you would create the folder path _Analyzer\EnsureModelDialogDisposed\TestData_ where the Analyzer is EnsureModalDialogDisposed for +this example. + +* Create a new TestClass, and give it a name which is describing the test class sufficiently. This + class derives from the base class `RoslynAnalyzerAndCodeFixTestBase` + for CSharp. Use the respective extension class for VB in addition, for getting the correct VB version. +* Since we're using for the actual tests + - `Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest` + - `Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest` + + or the VB equivalents, + + - `Microsoft.CodeAnalysis.VisualBasic.Testing.VisualBasicAnalyzerTest` + - `Microsoft.CodeAnalysis.VisualBasic.Testing.VisualBasicCodeFixTest` + you need to create a per test class a folder with a name which is the same as the name of the test class. And inside of that folder, + you create test data files which resemble CSharp or Visual Basic files, but are not part of the solution + (BuildAction: _None_, Copy to output directory: _Do not copy_). + Those files are: + - AnalyzerTestCode.cs: + A CSharp/VB file which contains the code which is used testing the Analyzer _on_. It should + contain code, which is triggering the Analyzer, or also makes sure, that in edge cases, the Analyzer is NOT triggered. + - An analyzer test, which does not rely on special CodeFix markers, can test the correctness of the Analyzer + by this: + +```CSharp + [Theory] + [CodeTestData(nameof(GetReferenceAssemblies))] + public async Task AvoidPassingTaskWithoutCancellationAnalyzer( + ReferenceAssemblies referenceAssemblies, + TestDataFileSet fileSet) + { + // Make sure, we can resolve the assembly we're testing against: + // Always pass `string.empty` for the language here to keep it generic. + var referenceAssembly = await referenceAssemblies.ResolveAsync( + language: string.Empty, + cancellationToken: CancellationToken.None); + + string diagnosticId = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken; + + var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(41, 21, 41, 97)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(44, 21, 44, 97)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(47, 21, 47, 98)); + + await context.RunAsync(); + } +``` + + - CodeFixTestCode.cs - Only for CodeFix scenarios: + A CSharp/VB file which contains the code which is used for the CodeFix test. Note, this file has the code parts tagged, + which are supposed to be fixed by the CodeFix. This file is used to verify that the CodeFix is working correctly. + Here is a sample of such a code file, with the respective Tags, which are '[|' and '|]'. + + ```CSharp + namespace CSharpControls; + +// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, +// since this is nothing our code fix touches. +public class ScalableControl : System.Windows.Forms.Control +{ + private SizeF _scaleSize = new SizeF(3, 14); + + /// + /// Sets or gets the scaled size of some foo bar thing. + /// + [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] + public SizeF [|ScaledSize|] + { + get => _scaleSize; + set => _scaleSize = value; + } + +public float [|ScaleFactor|] { get; set; } = 1.0f; + +/// +/// Sets or gets the scaled location of some foo bar thing. +/// +public PointF [|ScaledLocation|] +{ get; set; } + } + +``` + + - FixedTestCode.cs - Only for CodeFixes: + A CSharp/VB file which contains the code which is the code which has been fixed by the CodeFix. + This file is used to verify that the CodeFix is working correctly. + +For the above scenario, the fixed code would look like this: + +```CSharp +using System.ComponentModel; + +namespace CSharpControls; + +// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, +// since this is nothing our code fix touches. +public class ScalableControl : System.Windows.Forms.Control +{ + private SizeF _scaleSize = new SizeF(3, 14); + + /// + /// Sets or gets the scaled size of some foo bar thing. + /// + [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SizeF ScaledSize + { + get => _scaleSize; + set => _scaleSize = value; + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public float ScaleFactor { get; set; } = 1.0f; + + /// + /// Sets or gets the scaled location of some foo bar thing. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public PointF ScaledLocation +{ get; set; } + } +``` + +A sample test, which would use the above code would look like this: + +```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. +/// +/// +/// +/// This class is derived from "/> +/// and is intended to validate how properties are serialized in custom controls during +/// analyzer and code-fix operations. +/// +/// +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(); + } +} +``` + +* The Visual Basic equivalent of the above test class would look like this: + +```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.MissingPropertySerializationConfiguration +Imports System.Windows.Forms.VisualBasic.CodeFixes.AddDesignerSerializationVisibility +Imports Microsoft.CodeAnalysis.Testing +Imports Microsoft.WinForms.Test +Imports Microsoft.WinForms.Utilities.Shared +Imports Xunit + +Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.MissingPropertySerializationConfiguration + + ''' + ''' Represents a set of test scenarios for custom controls to verify + ''' property serialization behavior. + Public Class CustomControlScenarios + 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.Net9_0 + } + + For Each refAssembly In ReferenceAssemblyGenerator.GetForLatestTFMs(tfms) + Yield New Object() {refAssembly} + Next + End Function + + ''' + ''' Tests the diagnostics produced by + ''' . + ''' + + + Public Async Function TestDiagnostics( + referenceAssemblies As ReferenceAssemblies, + fileSet As TestDataFileSet) As Task + Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) + Await context.RunAsync() + + context = GetVisualBasicFixedTestContext(fileSet, referenceAssemblies) + Await context.RunAsync() + End Function + + ''' + ''' Tests the code-fix provider to ensure it correctly applies designer serialization attributes. + ''' + + + Public Async Function TestCodeFix( + referenceAssemblies As ReferenceAssemblies, + fileSet As TestDataFileSet) As Task + Dim context = GetVisualBasicCodeFixTestContext(Of AddDesignerSerializationVisibilityCodeFixProvider)( + fileSet, + referenceAssemblies, + numberOfFixAllIterations:=-2) + + context.CodeFixTestBehaviors = + CodeFixTestBehaviors.SkipFixAllInProjectCheck Or + CodeFixTestBehaviors.SkipFixAllInSolutionCheck + + Await context.RunAsync() + End Function + End Class + +End Namespace +``` + +Please note here the changed methods to get the test context for the VB tests: +`GetVisualBasicAnalyzerTestContext` and `GetVisualBasicCodeFixTestContext`. diff --git a/Winforms.sln b/Winforms.sln index 5107185a3af..2ec572b7066 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -179,6 +179,9 @@ EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "System.Windows.Forms.Analyzers.CodeFixes.VisualBasic", "src\System.Windows.Forms.Analyzers\vb\src\CodeFixes\System.Windows.Forms.Analyzers.CodeFixes.VisualBasic.vbproj", "{D9F59D9B-1A9E-D12E-9FEA-480FDCF3F6D2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzer", "Analyzer", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + AnalyzerTests-Copilot-Instructions.md = AnalyzerTests-Copilot-Instructions.md + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Windows.Forms.Analyzers.CSharp.Tests", "src\System.Windows.Forms.Analyzers\cs\tests\System.Windows.Forms.Analyzers.CSharp.Tests.csproj", "{6D3F4979-A444-778A-B6ED-6AA1786DADA0}" EndProject 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..9ee99c60469 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 @@ -62,22 +62,22 @@ - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never - PreserveNewest + Never AppConfigBuilder.cs From 7b2aaa079adf288af73fb1bdcfac598d9267331a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Mon, 31 Mar 2025 17:08:37 -0700 Subject: [PATCH 04/11] Update ReferenceAssemblyGenerator by LatestBuild Assembly resolver. --- AnalyzerTests-Copilot-Instructions.md | 340 ----------- Winforms.sln | 7 + .../tests/Microsoft.WinForms/NetVersion.cs | 23 +- ...mblyGenerator.WinFormsReferencesFactory.cs | 335 ++++++++++ .../ReferenceAssemblyGenerator.cs | 143 ++++- .../RoslynAnalyzerAndCodeFixTestBase.cs | 18 +- ...pertySerializationConfigurationAnalyzer.cs | 26 +- .../CustomControlScenarios.cs | 2 +- .../EdgeCaseScenarios.cs | 81 +++ .../EdgeCaseScenarios/AnalyzerTestCode.cs | 130 ++++ .../TestData/EdgeCaseScenarios/GlobalUsing.cs | 2 + .../TestData/EdgeCaseScenarios/Program.cs | 12 + ...indows.Forms.Analyzers.CSharp.Tests.csproj | 6 + .../AnalyzerTests-Copilot-Instructions.md | 576 ++++++++++++++++++ .../SamplePrompt for AnalyzerTests.md | 17 + 15 files changed, 1363 insertions(+), 355 deletions(-) delete mode 100644 AnalyzerTests-Copilot-Instructions.md create mode 100644 src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.WinFormsReferencesFactory.cs create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.cs create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.cs create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/GlobalUsing.cs create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.cs create mode 100644 src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md create mode 100644 src/System.Windows.Forms.Analyzers/prompting/SamplePrompt for AnalyzerTests.md diff --git a/AnalyzerTests-Copilot-Instructions.md b/AnalyzerTests-Copilot-Instructions.md deleted file mode 100644 index 2ec2584097c..00000000000 --- a/AnalyzerTests-Copilot-Instructions.md +++ /dev/null @@ -1,340 +0,0 @@ -# Copilot-Instructions to write test cases for WinForms Analyzers and CodeFixes - -For Analyzer tests, we have currently 3 different projects in the solution. -* System.Windows.Forms.Analyzers.CSharp.Tests -* System.Windows.Forms.Analyzers.VisualBasic.Tests -* System.Windows.Forms.Analyzers.Tests - -Important: -We are using AI currently only for adding tests to CSharp and VisualBasic tests projects. - -The approach is principally the same for both CSharp and VisualBasic. -But here are the important differences: - -* The CSharp tests are written in CSharp and are targeting the Analyzers which are also written in CSharp. -* When CSharp tests are using the test existing infrastructure, they are based on - - The namespaces around `Microsoft.CodeAnalysis...` - - The namespace `Microsoft.CodeAnalysis.CSharp.Testing` - - The test base class `RoslynAnalyzerAndCodeFixTestBase` - -* The VisualBasic tests are written in VisualBasic and are targeting the Analyzers which are also written in VisualBasic. -* When VisualBasic tests are using the test existing infrastructure, they are based on - - The namespaces around `Microsoft.CodeAnalysis...` - - The namespace `Microsoft.CodeAnalysis.VisualBasic.Testing` - - The test base class `RoslynAnalyzerAndCodeFixTestBase(Of TAnalyzer, DefaultVerifier)` - - But in addition the extension class `VisualBasicAnalyzerAndCodeFixExtensions`, so that the requirements for Visual Basic are met. - -* We always ask for CSharp and Visual Basic tests separately. If not specified and being ask - to write Analyzer tests, please refuse and ask to rephrase the request and specify the language. - -## General approach - -Inside of the respective test projects, a new folder for a new test series for an Analyzer should be -created in way, which is describing the Analyzer sufficiently, be under the folder `Analyzer`. Under that -folder, please create an additional folder `TestData` in addition. For example in _System.Windows.Forms.Analyzers.CSharp.Tests_ -you would create the folder path _Analyzer\EnsureModelDialogDisposed\TestData_ where the Analyzer is EnsureModalDialogDisposed for -this example. - -* Create a new TestClass, and give it a name which is describing the test class sufficiently. This - class derives from the base class `RoslynAnalyzerAndCodeFixTestBase` - for CSharp. Use the respective extension class for VB in addition, for getting the correct VB version. -* Since we're using for the actual tests - - `Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest` - - `Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest` - - or the VB equivalents, - - - `Microsoft.CodeAnalysis.VisualBasic.Testing.VisualBasicAnalyzerTest` - - `Microsoft.CodeAnalysis.VisualBasic.Testing.VisualBasicCodeFixTest` - you need to create a per test class a folder with a name which is the same as the name of the test class. And inside of that folder, - you create test data files which resemble CSharp or Visual Basic files, but are not part of the solution - (BuildAction: _None_, Copy to output directory: _Do not copy_). - Those files are: - - AnalyzerTestCode.cs: - A CSharp/VB file which contains the code which is used testing the Analyzer _on_. It should - contain code, which is triggering the Analyzer, or also makes sure, that in edge cases, the Analyzer is NOT triggered. - - An analyzer test, which does not rely on special CodeFix markers, can test the correctness of the Analyzer - by this: - -```CSharp - [Theory] - [CodeTestData(nameof(GetReferenceAssemblies))] - public async Task AvoidPassingTaskWithoutCancellationAnalyzer( - ReferenceAssemblies referenceAssemblies, - TestDataFileSet fileSet) - { - // Make sure, we can resolve the assembly we're testing against: - // Always pass `string.empty` for the language here to keep it generic. - var referenceAssembly = await referenceAssemblies.ResolveAsync( - language: string.Empty, - cancellationToken: CancellationToken.None); - - string diagnosticId = DiagnosticIDs.AvoidPassingFuncReturningTaskWithoutCancellationToken; - - var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(41, 21, 41, 97)); - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(44, 21, 44, 97)); - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(47, 21, 47, 98)); - - await context.RunAsync(); - } -``` - - - CodeFixTestCode.cs - Only for CodeFix scenarios: - A CSharp/VB file which contains the code which is used for the CodeFix test. Note, this file has the code parts tagged, - which are supposed to be fixed by the CodeFix. This file is used to verify that the CodeFix is working correctly. - Here is a sample of such a code file, with the respective Tags, which are '[|' and '|]'. - - ```CSharp - namespace CSharpControls; - -// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, -// since this is nothing our code fix touches. -public class ScalableControl : System.Windows.Forms.Control -{ - private SizeF _scaleSize = new SizeF(3, 14); - - /// - /// Sets or gets the scaled size of some foo bar thing. - /// - [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] - public SizeF [|ScaledSize|] - { - get => _scaleSize; - set => _scaleSize = value; - } - -public float [|ScaleFactor|] { get; set; } = 1.0f; - -/// -/// Sets or gets the scaled location of some foo bar thing. -/// -public PointF [|ScaledLocation|] -{ get; set; } - } - -``` - - - FixedTestCode.cs - Only for CodeFixes: - A CSharp/VB file which contains the code which is the code which has been fixed by the CodeFix. - This file is used to verify that the CodeFix is working correctly. - -For the above scenario, the fixed code would look like this: - -```CSharp -using System.ComponentModel; - -namespace CSharpControls; - -// We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, -// since this is nothing our code fix touches. -public class ScalableControl : System.Windows.Forms.Control -{ - private SizeF _scaleSize = new SizeF(3, 14); - - /// - /// Sets or gets the scaled size of some foo bar thing. - /// - [System.ComponentModel.Description("Sets or gets the scaled size of some foo bar thing.")] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public SizeF ScaledSize - { - get => _scaleSize; - set => _scaleSize = value; - } - - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public float ScaleFactor { get; set; } = 1.0f; - - /// - /// Sets or gets the scaled location of some foo bar thing. - /// - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public PointF ScaledLocation -{ get; set; } - } -``` - -A sample test, which would use the above code would look like this: - -```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. -/// -/// -/// -/// This class is derived from "/> -/// and is intended to validate how properties are serialized in custom controls during -/// analyzer and code-fix operations. -/// -/// -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(); - } -} -``` - -* The Visual Basic equivalent of the above test class would look like this: - -```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.MissingPropertySerializationConfiguration -Imports System.Windows.Forms.VisualBasic.CodeFixes.AddDesignerSerializationVisibility -Imports Microsoft.CodeAnalysis.Testing -Imports Microsoft.WinForms.Test -Imports Microsoft.WinForms.Utilities.Shared -Imports Xunit - -Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.MissingPropertySerializationConfiguration - - ''' - ''' Represents a set of test scenarios for custom controls to verify - ''' property serialization behavior. - Public Class CustomControlScenarios - 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.Net9_0 - } - - For Each refAssembly In ReferenceAssemblyGenerator.GetForLatestTFMs(tfms) - Yield New Object() {refAssembly} - Next - End Function - - ''' - ''' Tests the diagnostics produced by - ''' . - ''' - - - Public Async Function TestDiagnostics( - referenceAssemblies As ReferenceAssemblies, - fileSet As TestDataFileSet) As Task - Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) - Await context.RunAsync() - - context = GetVisualBasicFixedTestContext(fileSet, referenceAssemblies) - Await context.RunAsync() - End Function - - ''' - ''' Tests the code-fix provider to ensure it correctly applies designer serialization attributes. - ''' - - - Public Async Function TestCodeFix( - referenceAssemblies As ReferenceAssemblies, - fileSet As TestDataFileSet) As Task - Dim context = GetVisualBasicCodeFixTestContext(Of AddDesignerSerializationVisibilityCodeFixProvider)( - fileSet, - referenceAssemblies, - numberOfFixAllIterations:=-2) - - context.CodeFixTestBehaviors = - CodeFixTestBehaviors.SkipFixAllInProjectCheck Or - CodeFixTestBehaviors.SkipFixAllInSolutionCheck - - Await context.RunAsync() - End Function - End Class - -End Namespace -``` - -Please note here the changed methods to get the test context for the VB tests: -`GetVisualBasicAnalyzerTestContext` and `GetVisualBasicCodeFixTestContext`. diff --git a/Winforms.sln b/Winforms.sln index 2ec572b7066..6f15584c5e7 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -189,6 +189,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 @@ -1124,6 +1130,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/NetVersion.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/NetVersion.cs index e800a488d8e..639573d82ba 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,25 @@ 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 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..a1f5556c6ab 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,7 +20,7 @@ /// (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"; @@ -32,6 +33,84 @@ internal static class ReferenceAssemblyGenerator [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); + + // Microsoft.VisualBasic.Forms is at the top of the reference chain, + // so it has all the references we need. But we assert all the assemblies + // we need in addition to that. + 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; // Found the assembly, no need to check other folders + } + } + } + + return foundAssembliesBuilder.ToImmutable(); + } + /// /// Gets a reference assembly configuration for a specified .NET version. /// @@ -52,19 +131,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/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationAnalyzer.cs b/src/System.Windows.Forms.Analyzers/cs/src/Analyzers/MissingPropertySerializationConfiguration/MissingPropertySerializationConfigurationAnalyzer.cs index ec424a16424..6bc47a54000 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 @@ -25,7 +25,26 @@ public override void Initialize(AnalysisContext context) private static void AnalyzeSymbol(SymbolAnalysisContext context) { // We analyze only properties. - var propertySymbol = (IPropertySymbol)context.Symbol; + IPropertySymbol? propertySymbol = (IPropertySymbol)context.Symbol; + + // We never flag a property named Site of type of ISite + if (propertySymbol is null) + { + return; + } + + // A property of System.ComponentModel.ISite we never flag. + if (propertySymbol.Type.Name == nameof(ISite) + && propertySymbol.Type.ContainingNamespace.ToString() == "System.ComponentModel") + { + return; + } + + // 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 @@ -45,8 +64,9 @@ i.ContainingNamespace is not null && 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; 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 9ee99c60469..2cccb8e7922 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 @@ -43,6 +43,9 @@ + + + @@ -79,6 +82,9 @@ 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..a5793178589 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md @@ -0,0 +1,576 @@ +# 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 +Equivalent imports and program files in VB syntax as needed. + +## 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#. From 3ba49f3bc3196232751e792d2f3ee37a71d66346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Thu, 3 Apr 2025 17:16:09 -0700 Subject: [PATCH 05/11] Add InvokeAsync Analyzer fix both for VB and CSharp and Unittests. --- ...ingTaskWithoutCancellationTokenAnalyzer.cs | 38 ++++- .../ImplicitInvokeAsyncOnControl.cs | 68 ++++++++ .../AnalyzerTestCode.cs | 72 ++++++++ .../ImplicitInvokeAsyncOnControl/Program.cs | 16 ++ ...indows.Forms.Analyzers.CSharp.Tests.csproj | 4 + .../AnalyzerTests-Copilot-Instructions.md | 26 ++- ...ingTaskWithoutCancellationTokenAnalyzer.vb | 50 +++--- ...lizationConfigurationDiagnosticAnalyzer.vb | 28 +++- .../ImplicitInvokeAsyncOnControl.vb | 69 ++++++++ .../AnalyzerTestCode.vb | 69 ++++++++ .../ImplicitInvokeAsyncOnControl/Program.vb | 20 +++ .../EdgeCaseScenarios.vb | 77 +++++++++ .../EdgeCaseScenarios/AnalyzerTestCode.vb | 156 ++++++++++++++++++ .../TestData/EdgeCaseScenarios/Program.vb | 16 ++ ...s.Forms.Analyzers.VisualBasic.Tests.vbproj | 6 + 15 files changed, 683 insertions(+), 32 deletions(-) create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.cs create mode 100644 src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.cs create mode 100644 src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb create mode 100644 src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.vb create mode 100644 src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.vb create mode 100644 src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb create mode 100644 src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.vb create mode 100644 src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.vb 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/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs new file mode 100644 index 00000000000..41b074cd37f --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using 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.Net9_0 + ]; + + 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(31, 13, 31, 89)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(46, 13, 46, 89)); + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(74, 13, 74, 89)); + + 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..444d8e74c2e --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TestNamespace; + +public class MyForm : Form +{ + private async Task DoWorkWithoutThis() + { + // Case 1: Using InvokeAsync without 'this' in a synchronous context + // This should be detected by the analyzer + await InvokeAsync(ct => new ValueTask(DoSomethingAsync(ct)), CancellationToken.None); + + // Case 2: Using a variable instead of 'this', but not triggering the analyzer + CancellationToken token = new CancellationToken(); + await this.InvokeAsync(ct => DoSomethingWithToken(ct), token); + } + + private async Task DoWorkInNestedContext() + { + async Task LocalFunction() + { + // Case 3: Using InvokeAsync without 'this' in a nested function + // This should be detected by the analyzer + await InvokeAsync( + ct => new ValueTask(DoSomethingIntAsync(ct)), + CancellationToken.None); + } + + await LocalFunction(); + } + + // Helper methods for the test cases + private async Task DoSomethingAsync(CancellationToken token) + { + await Task.Delay(100, token); + return true; + } + + private ValueTask DoSomethingWithToken(CancellationToken token) + { + return new ValueTask(Task.CompletedTask); + } + + private async Task DoSomethingIntAsync(CancellationToken token) + { + await Task.Delay(100, token); + return 42; + } +} + +// Testing in a derived class to ensure the analyzer works with inheritance +public class DerivedForm : Form +{ + private async Task DoWorkInDerivedClass() + { + // Case 4: Using InvokeAsync without 'this' in a derived class + // This should be detected by the analyzer + await InvokeAsync(ct => new ValueTask(DoSomethingStringAsync(ct)), CancellationToken.None); + } + + 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..321075c4a43 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CSharpControls; + +public static class Program +{ + public static void Main(string[] args) + { + var control = new ScalableControl(); + + control.ScaleFactor = 1.5f; + control.ScaledSize = new SizeF(100, 100); + control.ScaledLocation = new PointF(10, 10); + } +} 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 2cccb8e7922..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 @@ + + @@ -63,6 +65,8 @@ + + Never diff --git a/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md b/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md index a5793178589..8f7a238d7b5 100644 --- a/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md +++ b/src/System.Windows.Forms.Analyzers/prompting/AnalyzerTests-Copilot-Instructions.md @@ -96,7 +96,31 @@ We currently have 3 different test projects in the solution: ``` ### For Visual Basic Tests -Equivalent imports and program files in VB syntax as needed. +* 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 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..2fbd41b9558 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb @@ -0,0 +1,69 @@ +' 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(27, 13, 27, 95)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(42, 17, 42, 99)) + context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(71, 13, 71, 95)) + + Await context.RunAsync() + End Function + End Class + +End Namespace 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..c8fde3af102 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/AnalyzerTestCode.vb @@ -0,0 +1,69 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Imports System +Imports System.Threading +Imports System.Threading.Tasks +Imports System.Windows.Forms + +Namespace TestNamespace + + Public Class MyForm + Inherits Form + + Private Async Function DoWorkWithoutMe() As Task + ' Case 1: Using InvokeAsync without 'Me' in a synchronous context + ' This should be detected by the analyzer + Await InvokeAsync(Function() DoSomethingAsync()) + + ' Case 2: Using a variable instead of 'Me', but not triggering the analyzer + Dim token As CancellationToken = New CancellationToken() + Await Me.InvokeAsync(Function(ct) DoSomethingWithToken(ct), token) + + End Function + + Private Async Function DoWorkInNestedContext() As Task + Await FunctionAsync() + End Function + + Private Async Function FunctionAsync() As Task + ' Case 3: Using InvokeAsync without 'Me' in a nested function + ' This should be detected by the analyzer + Await InvokeAsync( + New ValueTask(DoSomethingIntAsync), + CancellationToken.None) + End Function + + ' Helper methods for the test cases + Private Async Function DoSomethingAsync() As Task(Of Boolean) + Await Task.Delay(100) + Return True + End Function + + Private Function DoSomethingWithToken(token As CancellationToken) As ValueTask + Return New ValueTask(Task.CompletedTask) + End Function + + Private Function DoSomethingIntAsync(token As CancellationToken) As ValueTask(Of Integer) + Dim someTask = Task.Delay(100, token) + Return New ValueTask(someTask) + End Function + End Class + + ' Testing in a derived class to ensure the analyzer works with inheritance + Public Class DerivedForm + Inherits Form + + Private Async Function DoWorkInDerivedClass() As Task + ' Case 4: Using InvokeAsync without 'Me' in a derived class + ' This should be detected by the analyzer + Await InvokeAsync(Function(ct) New ValueTask(Of String)(DoSomethingStringAsync(ct)), CancellationToken.None) + 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..61ea9fef82c --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/ImplicitInvokeAsyncOnControl/Program.vb @@ -0,0 +1,20 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +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/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb new file mode 100644 index 00000000000..1655abdafc4 --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb @@ -0,0 +1,77 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Imports System.Threading +Imports System.Threading.Tasks +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) + + ' We expect no diagnostics for most edge cases + context.ExpectedDiagnostics.Clear() + + ' Only expect diagnostic on the one property that should be flagged + context.ExpectedDiagnostics.Add( + DiagnosticResult.CompilerError(diagnosticId).WithSpan(110, 25, 110, 40)) + + Await context.RunAsync() + End Function + End Class + +End Namespace 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..3e566fc665e --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/AnalyzerTestCode.vb @@ -0,0 +1,156 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +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..62133ef7dce --- /dev/null +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/TestData/EdgeCaseScenarios/Program.vb @@ -0,0 +1,16 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +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..7dd0ba564d7 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,14 @@ + + + @@ -50,11 +53,14 @@ + + + From 8bdf958221e4aaf62c983ad4be9a9b88ff226ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Thu, 3 Apr 2025 17:43:34 -0700 Subject: [PATCH 06/11] Update tests. --- .../ImplicitInvokeAsyncOnControl.cs | 2 +- .../TestData/ImplicitInvokeAsyncOnControl/Program.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) 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 index 41b074cd37f..3dcd8a39487 100644 --- a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs @@ -30,7 +30,7 @@ public static IEnumerable GetReferenceAssemblies() { NetVersion[] tfms = [ - NetVersion.Net9_0 + NetVersion.WinFormsBuild ]; foreach (ReferenceAssemblies refAssembly in ReferenceAssemblyGenerator.GetForLatestTFMs(tfms)) 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 index 321075c4a43..c1030d704f6 100644 --- 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 @@ -3,14 +3,13 @@ namespace CSharpControls; +using System.Windows.Forms; + public static class Program { public static void Main(string[] args) { - var control = new ScalableControl(); - - control.ScaleFactor = 1.5f; - control.ScaledSize = new SizeF(100, 100); - control.ScaledLocation = new PointF(10, 10); + var form = new TestNamespace.MyForm(); + Application.Run(form); } } From 44368792b5cd888c86a20dfc48899ea06261bbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Thu, 3 Apr 2025 22:58:18 -0700 Subject: [PATCH 07/11] Fix VB tests and test data. --- .../ImplicitInvokeAsyncOnControl.cs | 8 +- .../AnalyzerTestCode.cs | 56 +++++++------ .../ImplicitInvokeAsyncOnControl.vb | 8 +- .../AnalyzerTestCode.vb | 83 ++++++++++++------- ...s.Forms.Analyzers.VisualBasic.Tests.vbproj | 3 + 5 files changed, 97 insertions(+), 61 deletions(-) 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 index 3dcd8a39487..108de1bb566 100644 --- a/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs +++ b/src/System.Windows.Forms.Analyzers/cs/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.cs @@ -59,9 +59,11 @@ public async Task DetectImplicitInvokeAsyncCalls( var context = GetAnalyzerTestContext(fileSet, referenceAssemblies); // Explicitly specify where diagnostics are expected - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(31, 13, 31, 89)); - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(46, 13, 46, 89)); - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(74, 13, 74, 89)); + 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 index 444d8e74c2e..452ec432524 100644 --- 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 @@ -1,54 +1,56 @@ // 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.Windows.Forms; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; namespace TestNamespace; public class MyForm : Form { - private async Task DoWorkWithoutThis() + internal async Task DoWorkWithoutThis() { - // Case 1: Using InvokeAsync without 'this' in a synchronous context - // This should be detected by the analyzer - await InvokeAsync(ct => new ValueTask(DoSomethingAsync(ct)), CancellationToken.None); - - // Case 2: Using a variable instead of 'this', but not triggering the analyzer - CancellationToken token = new CancellationToken(); - await this.InvokeAsync(ct => DoSomethingWithToken(ct), token); + // 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() { - // Case 3: Using InvokeAsync without 'this' in a nested function - // This should be detected by the analyzer - await InvokeAsync( - ct => new ValueTask(DoSomethingIntAsync(ct)), - CancellationToken.None); + // Make sure we detect this inside of a nested local function. + await InvokeAsync(async () => await Task.Delay(100)); } - - await LocalFunction(); } // Helper methods for the test cases private async Task DoSomethingAsync(CancellationToken token) { - await Task.Delay(100, token); + await Task.Delay(42 + 73, token); return true; } - private ValueTask DoSomethingWithToken(CancellationToken token) + private async ValueTask DoSomethingWithTokenAsync(CancellationToken token) { - return new ValueTask(Task.CompletedTask); + bool flag = await DoSomethingAsync(token); + var meaningOfLife = 21 + 21; + + return (meaningOfLife == await GoRateotuAsync(token)) && flag; } - private async Task DoSomethingIntAsync(CancellationToken token) + private async Task GoRateotuAsync(CancellationToken token) { + DerivedForm derivedForm = new(); + await derivedForm.DoWorkInDerivedClassAsync(); + await Task.Delay(100, token); return 42; } @@ -57,11 +59,15 @@ private async Task DoSomethingIntAsync(CancellationToken token) // Testing in a derived class to ensure the analyzer works with inheritance public class DerivedForm : Form { - private async Task DoWorkInDerivedClass() + internal async Task DoWorkInDerivedClassAsync() { - // Case 4: Using InvokeAsync without 'this' in a derived class - // This should be detected by the analyzer - await InvokeAsync(ct => new ValueTask(DoSomethingStringAsync(ct)), CancellationToken.None); + 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) 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 index 2fbd41b9558..fa2a2fb0332 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/ImplicitInvokeAsyncOnControl.vb @@ -58,9 +58,11 @@ Namespace System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests.AvoidPa Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) ' Explicitly specify where diagnostics are expected - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(27, 13, 27, 95)) - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(42, 17, 42, 99)) - context.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning(diagnosticId).WithSpan(71, 13, 71, 95)) + 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 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 index c8fde3af102..e5053f63d8c 100644 --- 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 @@ -1,52 +1,67 @@ ' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. -Imports System +Option Strict On +Option Explicit On + +Imports System.Windows.Forms Imports System.Threading Imports System.Threading.Tasks -Imports System.Windows.Forms Namespace TestNamespace - Public Class MyForm Inherits Form - Private Async Function DoWorkWithoutMe() As Task - ' Case 1: Using InvokeAsync without 'Me' in a synchronous context - ' This should be detected by the analyzer - Await InvokeAsync(Function() DoSomethingAsync()) - - ' Case 2: Using a variable instead of 'Me', but not triggering the analyzer - Dim token As CancellationToken = New CancellationToken() - Await Me.InvokeAsync(Function(ct) DoSomethingWithToken(ct), token) + 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 FunctionAsync() - End Function - Private Async Function FunctionAsync() As Task - ' Case 3: Using InvokeAsync without 'Me' in a nested function - ' This should be detected by the analyzer + Await NestedFunction() + Await InvokeAsync( - New ValueTask(DoSomethingIntAsync), + 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() As Task(Of Boolean) - Await Task.Delay(100) - Return True + Private Async Function DoSomethingAsync(token As CancellationToken) As Task + Await Task.Delay(42 + 73, token) End Function - Private Function DoSomethingWithToken(token As CancellationToken) As ValueTask - Return New ValueTask(Task.CompletedTask) + 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 GoRateotuAsync(token) End Function - Private Function DoSomethingIntAsync(token As CancellationToken) As ValueTask(Of Integer) - Dim someTask = Task.Delay(100, token) - Return New ValueTask(someTask) + Private Async Function GoRateotuAsync(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 @@ -54,10 +69,19 @@ Namespace TestNamespace Public Class DerivedForm Inherits Form - Private Async Function DoWorkInDerivedClass() As Task - ' Case 4: Using InvokeAsync without 'Me' in a derived class - ' This should be detected by the analyzer - Await InvokeAsync(Function(ct) New ValueTask(Of String)(DoSomethingStringAsync(ct)), CancellationToken.None) + 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) @@ -65,5 +89,4 @@ Namespace TestNamespace Return "test" End Function 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 7dd0ba564d7..e6423a80b50 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,6 +11,7 @@ + @@ -53,6 +54,7 @@ + @@ -63,4 +65,5 @@ + From cc55027359bb81cd720678d7ab1732cecf71d01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Tue, 8 Apr 2025 16:10:01 -0700 Subject: [PATCH 08/11] Clean up comments and VB test data code. --- .../CodeTestDataAttribute.cs | 2 - .../tests/Microsoft.WinForms/NetVersion.cs | 3 +- .../ReferenceAssemblyGenerator.cs | 11 +++-- .../AnalyzerTestCode.cs | 4 +- .../InvokeAsyncOnControl.vb | 1 + .../AnalyzerTestCode.vb | 5 ++- .../ImplicitInvokeAsyncOnControl/Program.vb | 3 ++ .../InvokeAsyncOnControl/AnalyzerTestCode.vb | 41 +++++++++++-------- .../EdgeCaseScenarios.vb | 3 -- .../AnalyzerTestCode.vb | 3 ++ .../CustomControlScenarios/CodeFixTestCode.vb | 3 ++ .../CustomControlScenarios/FixedTestCode.vb | 3 ++ .../CustomControlScenarios/Program.vb | 3 ++ .../EdgeCaseScenarios/AnalyzerTestCode.vb | 3 ++ .../TestData/EdgeCaseScenarios/Program.vb | 3 ++ ...s.Forms.Analyzers.VisualBasic.Tests.vbproj | 1 + 16 files changed, 61 insertions(+), 31 deletions(-) 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 639573d82ba..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 @@ -21,7 +21,8 @@ internal enum NetVersion WinFormsBuild = 0x01000000, /// - /// If this is OR'ed in, we're taking the specified version, and the WinForms runtime for this repo. + /// 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.cs b/src/System.Windows.Forms.Analyzers/common/tests/Microsoft.WinForms/ReferenceAssemblyGenerator.cs index a1f5556c6ab..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 @@ -27,6 +27,12 @@ internal static partial class ReferenceAssemblyGenerator 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"), @@ -58,9 +64,6 @@ public static ImmutableArray GetWinFormsBuildAssemblies(string tfm, stri var winFormsAssembliesBuilder = ImmutableArray.CreateBuilder(s_winFormsAssemblies.Count); - // Microsoft.VisualBasic.Forms is at the top of the reference chain, - // so it has all the references we need. But we assert all the assemblies - // we need in addition to that. string fullAssemblyPath = Path.Join( WinFormsReferencesFactory.RepoRootPath, "artifacts\\bin"); @@ -103,7 +106,7 @@ private static ImmutableArray FindAssembliesInSubfolders( if (File.Exists(assemblyPath)) { foundAssembliesBuilder.Add(assemblyPath); - break; // Found the assembly, no need to check other folders + break; } } } 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 index 452ec432524..cd035c7e6d8 100644 --- 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 @@ -43,10 +43,10 @@ private async ValueTask DoSomethingWithTokenAsync(CancellationToken token) bool flag = await DoSomethingAsync(token); var meaningOfLife = 21 + 21; - return (meaningOfLife == await GoRateotuAsync(token)) && flag; + return (meaningOfLife == await GetMeaningOfLifeAsync(token)) && flag; } - private async Task GoRateotuAsync(CancellationToken token) + private async Task GetMeaningOfLifeAsync(CancellationToken token) { DerivedForm derivedForm = new(); await derivedForm.DoWorkInDerivedClassAsync(); 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..a290b209fb0 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( 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 index e5053f63d8c..a8b656774f7 100644 --- 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 @@ -53,10 +53,10 @@ Namespace TestNamespace Await DoSomethingAsync(token) Dim meaningOfLife As Integer = 21 + 21 - Return meaningOfLife = Await GoRateotuAsync(token) + Return meaningOfLife = Await GetMeaningOfLifeAsync(token) End Function - Private Async Function GoRateotuAsync(token As CancellationToken) As Task(Of Integer) + Private Async Function GetMeaningOfLifeAsync(token As CancellationToken) As Task(Of Integer) Dim derivedForm As New DerivedForm() Await derivedForm.DoWorkInDerivedClassAsync() @@ -88,5 +88,6 @@ Namespace TestNamespace 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 index 61ea9fef82c..c1b2796be96 100644 --- 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 @@ -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 Imports System.Windows.Forms 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..71dd443032e 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 @@ -3,6 +3,9 @@ Imports System.Threading Imports System.Threading.Tasks Imports System.Windows.Forms +Option Strict On +Option Explicit On + Namespace VisualBasicControls Public Module Program @@ -15,17 +18,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 +48,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 index 1655abdafc4..633e4b3c31f 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb @@ -1,8 +1,6 @@ ' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. -Imports System.Threading -Imports System.Threading.Tasks Imports System.Windows.Forms.Analyzers.Diagnostics Imports System.Windows.Forms.Analyzers.Tests.Microsoft.WinForms Imports System.Windows.Forms.VisualBasic.Analyzers.MissingPropertySerializationConfiguration @@ -63,7 +61,6 @@ Namespace Global.System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests. Dim context = GetVisualBasicAnalyzerTestContext(fileSet, referenceAssemblies) - ' We expect no diagnostics for most edge cases context.ExpectedDiagnostics.Clear() ' Only expect diagnostic on the one property that should be flagged 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..088bd07d3b1 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,6 +1,9 @@ Imports System.ComponentModel Imports System.Drawing +Option Strict On +Option Explicit On + Namespace VisualBasicControls ' We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, 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..bbe08776316 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,6 +1,9 @@ Imports System.ComponentModel Imports System.Drawing +Option Strict On +Option Explicit On + Namespace VisualBasicControls ' We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, 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..d0ba00bf982 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,6 +1,9 @@ Imports System.ComponentModel Imports System.Drawing +Option Strict On +Option Explicit On + Namespace VisualBasicControls ' We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, 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 index 3e566fc665e..7159659880e 100644 --- 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 @@ -4,6 +4,9 @@ Imports System Imports System.ComponentModel +Option Strict On +Option Explicit On + Namespace Test ' Custom IComponent interface in a different 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 index 62133ef7dce..440b0d31830 100644 --- 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 @@ -3,6 +3,9 @@ Imports System +Option Strict On +Option Explicit On + Namespace Test Friend NotInheritable Class Program ''' 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 e6423a80b50..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 @@ -20,6 +20,7 @@ + From 3ee61990d1350cb82c5cfbf952f83566b640104e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Wed, 16 Apr 2025 17:45:00 -0700 Subject: [PATCH 09/11] Fix Option Explicit placing in code. --- .../InvokeAsyncOnControl.vb | 6 +++--- .../TestData/InvokeAsyncOnControl/AnalyzerTestCode.vb | 11 +++++++---- .../EdgeCaseScenarios.vb | 2 +- .../CustomControlScenarios/AnalyzerTestCode.vb | 8 ++++---- .../CustomControlScenarios/CodeFixTestCode.vb | 8 ++++---- .../TestData/CustomControlScenarios/FixedTestCode.vb | 8 ++++---- .../TestData/EdgeCaseScenarios/AnalyzerTestCode.vb | 5 ++--- .../TestData/EdgeCaseScenarios/Program.vb | 4 ++-- 8 files changed, 27 insertions(+), 25 deletions(-) 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 a290b209fb0..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 @@ -43,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/InvokeAsyncOnControl/AnalyzerTestCode.vb b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/AvoidPassingTaskWithoutCancellationToken/TestData/InvokeAsyncOnControl/AnalyzerTestCode.vb index 71dd443032e..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,11 +1,14 @@ -Imports System -Imports System.Threading -Imports System.Threading.Tasks -Imports System.Windows.Forms +' 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 + Namespace VisualBasicControls Public Module Program 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 index 633e4b3c31f..9f4908e0594 100644 --- a/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb +++ b/src/System.Windows.Forms.Analyzers/vb/tests/Analyzer/MissingPropertySerializationConfiguration/EdgeCaseScenarios.vb @@ -65,7 +65,7 @@ Namespace Global.System.Windows.Forms.Analyzers.VisualBasic.Tests.AnalyzerTests. ' Only expect diagnostic on the one property that should be flagged context.ExpectedDiagnostics.Add( - DiagnosticResult.CompilerError(diagnosticId).WithSpan(110, 25, 110, 40)) + DiagnosticResult.CompilerError(diagnosticId).WithSpan(112, 25, 112, 40)) Await context.RunAsync() End Function 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 088bd07d3b1..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,9 +1,9 @@ -Imports System.ComponentModel -Imports System.Drawing - -Option Strict On +Option Strict On Option Explicit On +Imports System.ComponentModel +Imports System.Drawing + Namespace VisualBasicControls ' We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, 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 bbe08776316..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,9 +1,9 @@ -Imports System.ComponentModel -Imports System.Drawing - -Option Strict On +Option Strict On Option Explicit On +Imports System.ComponentModel +Imports System.Drawing + Namespace VisualBasicControls ' We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, 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 d0ba00bf982..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,9 +1,9 @@ -Imports System.ComponentModel -Imports System.Drawing - -Option Strict On +Option Strict On Option Explicit On +Imports System.ComponentModel +Imports System.Drawing + Namespace VisualBasicControls ' We are writing the fully-qualified name here to make sure, the Simplifier doesn't remove it, 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 index 7159659880e..2cde88f2b7c 100644 --- 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 @@ -1,12 +1,11 @@ ' 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 -Option Strict On -Option Explicit On - Namespace Test ' Custom IComponent interface in a different 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 index 440b0d31830..b561b457ae0 100644 --- 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 @@ -1,11 +1,11 @@ ' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. -Imports System - Option Strict On Option Explicit On +Imports System + Namespace Test Friend NotInheritable Class Program ''' From dc7d6cacc0f8561c12b683bc1be64c8091c68b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Thu, 17 Apr 2025 10:21:57 -0700 Subject: [PATCH 10/11] Update prompt-folder for Analyzer test generation. --- Winforms.sln | 3 --- 1 file changed, 3 deletions(-) diff --git a/Winforms.sln b/Winforms.sln index 6f15584c5e7..4232199c7a1 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -179,9 +179,6 @@ EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "System.Windows.Forms.Analyzers.CodeFixes.VisualBasic", "src\System.Windows.Forms.Analyzers\vb\src\CodeFixes\System.Windows.Forms.Analyzers.CodeFixes.VisualBasic.vbproj", "{D9F59D9B-1A9E-D12E-9FEA-480FDCF3F6D2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzer", "Analyzer", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" - ProjectSection(SolutionItems) = preProject - AnalyzerTests-Copilot-Instructions.md = AnalyzerTests-Copilot-Instructions.md - EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Windows.Forms.Analyzers.CSharp.Tests", "src\System.Windows.Forms.Analyzers\cs\tests\System.Windows.Forms.Analyzers.CSharp.Tests.csproj", "{6D3F4979-A444-778A-B6ED-6AA1786DADA0}" EndProject From 80c449e9a504df76d36fb3419a8959e981ba33b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Mon, 21 Apr 2025 17:42:36 -0700 Subject: [PATCH 11/11] Address review feedback. --- ...gPropertySerializationConfigurationAnalyzer.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) 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 6bc47a54000..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,11 +24,9 @@ public override void Initialize(AnalysisContext context) private static void AnalyzeSymbol(SymbolAnalysisContext context) { - // We analyze only properties. - IPropertySymbol? propertySymbol = (IPropertySymbol)context.Symbol; - - // We never flag a property named Site of type of ISite - if (propertySymbol is null) + // We only care about properties, and don't care about static properties. + if (context.Symbol is not IPropertySymbol propertySymbol + || propertySymbol.IsStatic) { return; } @@ -58,12 +56,6 @@ i.ContainingNamespace is not null && return; } - // Skip static properties since they are not serialized by the designer - if (propertySymbol.IsStatic) - { - return; - } - // 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 @@ -73,7 +65,6 @@ i.ContainingNamespace is not null && } // Skip overridden properties since the base property should already have the appropriate serialization configuration - // TODO: Does just this cover all the cases, particularly for legacy code where the dev doesn't own the source? if (propertySymbol.IsOverride) { return;