From 2eb6613bcc575167a6f051b618645949db49d468 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Thu, 7 Aug 2025 11:50:14 +0000 Subject: [PATCH 01/38] SLVS-2411 Create empty project (#6366) --- SonarQube.VisualStudio.sln | 36 ++++ .../GlobalUsings.cs | 25 +++ ...slynAnalyzerServer.IntegrationTests.csproj | 15 ++ .../packages.lock.json | 156 ++++++++++++++++++ .../GlobalUsings.cs | 25 +++ .../RoslynAnalyzerServer.UnitTests.csproj | 15 ++ .../packages.lock.json | 156 ++++++++++++++++++ .../RoslynAnalyzerServer.csproj | 15 ++ src/RoslynAnalyzerServer/packages.lock.json | 81 +++++++++ 9 files changed, 524 insertions(+) create mode 100644 src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs create mode 100644 src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj create mode 100644 src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json create mode 100644 src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj create mode 100644 src/RoslynAnalyzerServer.UnitTests/packages.lock.json create mode 100644 src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj create mode 100644 src/RoslynAnalyzerServer/packages.lock.json diff --git a/SonarQube.VisualStudio.sln b/SonarQube.VisualStudio.sln index 15071cab56..8e7584bf23 100644 --- a/SonarQube.VisualStudio.sln +++ b/SonarQube.VisualStudio.sln @@ -22,6 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets nuget.config = nuget.config + src\RoslynAnalyzerServer\RoslynAnalyzerServer.csproj = src\RoslynAnalyzerServer\RoslynAnalyzerServer.csproj SonarLint.VsTargetVersion.props = SonarLint.VsTargetVersion.props EndProjectSection EndProject @@ -134,6 +135,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopyDependencies", "build\C {3ECCAF9D-3B23-4980-83E2-8ACEE0FC6BEB} = {3ECCAF9D-3B23-4980-83E2-8ACEE0FC6BEB} EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RoslynAnalyzerServer", "RoslynAnalyzerServer", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynAnalyzerServer", "src\RoslynAnalyzerServer\RoslynAnalyzerServer.csproj", "{7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynAnalyzerServer.UnitTests", "src\RoslynAnalyzerServer.UnitTests\RoslynAnalyzerServer.UnitTests.csproj", "{754189A4-1458-4F0C-9D1A-E4F359B66E37}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynAnalyzerServer.IntegrationTests", "src\RoslynAnalyzerServer.IntegrationTests\RoslynAnalyzerServer.IntegrationTests.csproj", "{DCC4771A-C097-4B46-B1EB-39C3E13C6252}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -382,6 +391,30 @@ Global {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44}.Release|Any CPU.Build.0 = Debug|x86 {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44}.Release|x86.ActiveCfg = Debug|x86 {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44}.Release|x86.Build.0 = Debug|x86 + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Debug|x86.Build.0 = Debug|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|Any CPU.Build.0 = Release|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|x86.ActiveCfg = Release|Any CPU + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4}.Release|x86.Build.0 = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|x86.ActiveCfg = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Debug|x86.Build.0 = Debug|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|Any CPU.Build.0 = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|x86.ActiveCfg = Release|Any CPU + {754189A4-1458-4F0C-9D1A-E4F359B66E37}.Release|x86.Build.0 = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|x86.ActiveCfg = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Debug|x86.Build.0 = Debug|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|Any CPU.Build.0 = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|x86.ActiveCfg = Release|Any CPU + {DCC4771A-C097-4B46-B1EB-39C3E13C6252}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -422,6 +455,9 @@ Global {1625398B-2343-481E-9B90-57B38EBEE8C5} = {236587E8-62A7-4E4E-815D-A50433859DC7} {421D8026-2CBF-4444-A886-67428C1813E9} = {E93C2CF9-69A6-4669-BE8A-6060B18FEDCA} {2BB16C6F-BF06-4225-99A0-F1CFE70CBE44} = {421D8026-2CBF-4444-A886-67428C1813E9} + {7EDBF3B2-2820-4C35-A368-CE3213F0BBE4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {754189A4-1458-4F0C-9D1A-E4F359B66E37} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DCC4771A-C097-4B46-B1EB-39C3E13C6252} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DABC27C8-E761-4826-AD2D-056F677EF3C0} diff --git a/src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs b/src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs new file mode 100644 index 0000000000..8e8fa28ddd --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/GlobalUsings.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +global using System; +global using FluentAssertions; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using NSubstitute; diff --git a/src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj b/src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj new file mode 100644 index 0000000000..74582bc764 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/RoslynAnalyzerServer.IntegrationTests.csproj @@ -0,0 +1,15 @@ + + + + + + SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests + SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests + enable + + + + + + + diff --git a/src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json b/src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json new file mode 100644 index 0000000000..4e6a6dffb0 --- /dev/null +++ b/src/RoslynAnalyzerServer.IntegrationTests/packages.lock.json @@ -0,0 +1,156 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.8": { + "FluentAssertions": { + "type": "Direct", + "requested": "[5.9.0, )", + "resolved": "5.9.0", + "contentHash": "JZxb5DuspmuH4A5p9bfj060evQnwIUkxxfFMG9F/pa7gl1Ry2r2nuQbu5G4UWGQQTtpTxPpVowqNDdBkA0G2Mw==" + }, + "FluentAssertions.Analyzers": { + "type": "Direct", + "requested": "[0.11.4, )", + "resolved": "0.11.4", + "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[16.6.1, )", + "resolved": "16.6.1", + "contentHash": "zYAjfWzpxKb64P9ntReT1Xr8HdONZnpLVs12HIjXWo+UOCDpevP1UWRoaAgNysaD1/l3teBKvgbSeG9bRssfOQ==", + "dependencies": { + "Microsoft.CodeCoverage": "16.6.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.18.2, )", + "resolved": "4.18.2", + "contentHash": "SjxKYS5nX6prcaT8ZjbkONh3vnh0Rxru09+gQ1a07v4TM530Oe/jq3Q4dOZPfo1wq0LYmTgLOZKrqRfEx4auPw==", + "dependencies": { + "Castle.Core": "5.1.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "t2/rL9DG+cVAgPs98OGm2sbZ4FTgn+MGEan5P9NRAgqMV3+nYRKG7/5R0jY7lBMq9ISms+84MSqTHWs6QnPt4A==" + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "kV/yZ0XLiOElsVeLT0GnNrsoKcPvVNOP6Cv2zkAiceJY0rpro0L+3t54bRApLwTg1mxlo4rLziBG7X6X69KcrQ==" + }, + "NSubstitute": { + "type": "Direct", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "NSubstitute.Analyzers.CSharp": { + "type": "Direct", + "requested": "[1.0.17, )", + "resolved": "1.0.17", + "contentHash": "Pwz0MD7CAM/G/fvJjM3ceOfI+S0IgjanHcK7evwyrW9qAWUG8fgiEXYfSX1/s3h2JUNDOw6ik0G8zp+RT61Y1g==" + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "16.6.1", + "contentHash": "nBYXDgAZCfjsOVzlhMB5olGvX4dTDWB/gWaYS/MhgXBcCz8XJuVGqkfK8LmwlBR/eeUPE9Q/NFZNwlJyMZf0vg==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", + "dependencies": { + "System.Memory": "4.5.4" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "SonarLint.VisualStudio.Core": { + "type": "Project", + "dependencies": { + "BouncyCastle.Cryptography": "[2.4.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "System.Collections.Immutable": "[5.0.0, )", + "System.IO.Abstractions": "[9.0.4, )", + "System.Threading.Channels": "[7.0.0, )" + } + }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs b/src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000000..8e8fa28ddd --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/GlobalUsings.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +global using System; +global using FluentAssertions; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using NSubstitute; diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj new file mode 100644 index 0000000000..e74cffb044 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj @@ -0,0 +1,15 @@ + + + + + + SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests + SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests + enable + + + + + + + diff --git a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json new file mode 100644 index 0000000000..4e6a6dffb0 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json @@ -0,0 +1,156 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.8": { + "FluentAssertions": { + "type": "Direct", + "requested": "[5.9.0, )", + "resolved": "5.9.0", + "contentHash": "JZxb5DuspmuH4A5p9bfj060evQnwIUkxxfFMG9F/pa7gl1Ry2r2nuQbu5G4UWGQQTtpTxPpVowqNDdBkA0G2Mw==" + }, + "FluentAssertions.Analyzers": { + "type": "Direct", + "requested": "[0.11.4, )", + "resolved": "0.11.4", + "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[16.6.1, )", + "resolved": "16.6.1", + "contentHash": "zYAjfWzpxKb64P9ntReT1Xr8HdONZnpLVs12HIjXWo+UOCDpevP1UWRoaAgNysaD1/l3teBKvgbSeG9bRssfOQ==", + "dependencies": { + "Microsoft.CodeCoverage": "16.6.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.18.2, )", + "resolved": "4.18.2", + "contentHash": "SjxKYS5nX6prcaT8ZjbkONh3vnh0Rxru09+gQ1a07v4TM530Oe/jq3Q4dOZPfo1wq0LYmTgLOZKrqRfEx4auPw==", + "dependencies": { + "Castle.Core": "5.1.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "t2/rL9DG+cVAgPs98OGm2sbZ4FTgn+MGEan5P9NRAgqMV3+nYRKG7/5R0jY7lBMq9ISms+84MSqTHWs6QnPt4A==" + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "kV/yZ0XLiOElsVeLT0GnNrsoKcPvVNOP6Cv2zkAiceJY0rpro0L+3t54bRApLwTg1mxlo4rLziBG7X6X69KcrQ==" + }, + "NSubstitute": { + "type": "Direct", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "dependencies": { + "Castle.Core": "5.1.1", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "NSubstitute.Analyzers.CSharp": { + "type": "Direct", + "requested": "[1.0.17, )", + "resolved": "1.0.17", + "contentHash": "Pwz0MD7CAM/G/fvJjM3ceOfI+S0IgjanHcK7evwyrW9qAWUG8fgiEXYfSX1/s3h2JUNDOw6ik0G8zp+RT61Y1g==" + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "16.6.1", + "contentHash": "nBYXDgAZCfjsOVzlhMB5olGvX4dTDWB/gWaYS/MhgXBcCz8XJuVGqkfK8LmwlBR/eeUPE9Q/NFZNwlJyMZf0vg==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", + "dependencies": { + "System.Memory": "4.5.4" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "SonarLint.VisualStudio.Core": { + "type": "Project", + "dependencies": { + "BouncyCastle.Cryptography": "[2.4.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "System.Collections.Immutable": "[5.0.0, )", + "System.IO.Abstractions": "[9.0.4, )", + "System.Threading.Channels": "[7.0.0, )" + } + }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj new file mode 100644 index 0000000000..31f22c5b7b --- /dev/null +++ b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj @@ -0,0 +1,15 @@ + + + + + + SonarLint.VisualStudio.RoslynAnalyzerServer + SonarLint.VisualStudio.RoslynAnalyzerServer + enable + + + + + + + diff --git a/src/RoslynAnalyzerServer/packages.lock.json b/src/RoslynAnalyzerServer/packages.lock.json new file mode 100644 index 0000000000..e6196c9a2e --- /dev/null +++ b/src/RoslynAnalyzerServer/packages.lock.json @@ -0,0 +1,81 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.7.2": { + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", + "dependencies": { + "System.Memory": "4.5.4" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "SonarLint.VisualStudio.Core": { + "type": "Project", + "dependencies": { + "BouncyCastle.Cryptography": "[2.4.0, )", + "Newtonsoft.Json": "[13.0.3, )", + "System.Collections.Immutable": "[5.0.0, )", + "System.IO.Abstractions": "[9.0.4, )", + "System.Threading.Channels": "[7.0.0, )" + } + } + } + } +} \ No newline at end of file From f543fdbc9635c318e5a2b42ce42a87f2692120be Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:24:49 +0200 Subject: [PATCH 02/38] SLVS-2464 Fix new project references & add dependencies (#6367) --- src/CFamily.UnitTests/packages.lock.json | 7 + .../packages.lock.json | 7 + src/Integration.Vsix/Integration.Vsix.csproj | 5 + .../VS2022/source.extension.vsixmanifest | 1 + src/Integration.Vsix/packages.lock.json | 6 + .../InternalsVisibleTo.cs | 36 +++++ .../RoslynAnalyzerServer.csproj | 10 ++ src/RoslynAnalyzerServer/packages.lock.json | 124 +++++++++++++++++- .../packages.lock.json | 7 + 9 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 src/RoslynAnalyzerServer/InternalsVisibleTo.cs diff --git a/src/CFamily.UnitTests/packages.lock.json b/src/CFamily.UnitTests/packages.lock.json index 975bb3b8e6..38a4a6ff78 100644 --- a/src/CFamily.UnitTests/packages.lock.json +++ b/src/CFamily.UnitTests/packages.lock.json @@ -1261,6 +1261,7 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", "SonarQube.Client": "[1.0.0, )", @@ -1388,6 +1389,12 @@ "System.IO.Abstractions": "[9.0.4, )" } }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + }, "SonarLint.VisualStudio.SLCore": { "type": "Project", "dependencies": { diff --git a/src/Integration.Vsix.UnitTests/packages.lock.json b/src/Integration.Vsix.UnitTests/packages.lock.json index a8135ba20b..dc86230faa 100644 --- a/src/Integration.Vsix.UnitTests/packages.lock.json +++ b/src/Integration.Vsix.UnitTests/packages.lock.json @@ -1266,6 +1266,7 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", "SonarQube.Client": "[1.0.0, )", @@ -1393,6 +1394,12 @@ "System.IO.Abstractions": "[9.0.4, )" } }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + }, "SonarLint.VisualStudio.SLCore": { "type": "Project", "dependencies": { diff --git a/src/Integration.Vsix/Integration.Vsix.csproj b/src/Integration.Vsix/Integration.Vsix.csproj index 78184e93cd..a93a2827bc 100644 --- a/src/Integration.Vsix/Integration.Vsix.csproj +++ b/src/Integration.Vsix/Integration.Vsix.csproj @@ -132,6 +132,11 @@ BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b DebugSymbolsProjectOutputGroup%3b + + RoslynAnalyzerServer + BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b + DebugSymbolsProjectOutputGroup%3b + diff --git a/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest b/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest index 71680086de..0d2d928242 100644 --- a/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest +++ b/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest @@ -39,6 +39,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Content length exceeded. Received {0} bytes, max allowed: {1} bytes. + + + Too many concurrent requests. Max allowed {0}. + + + Error handling request: {0}. + + + The analysis request timed out after {0} ms. + + + Attempt {0} to start server on port {1} failed due to {2}. + + + Http Server + + + Server stopped and resources disposed. + + + Failed to start server after {0} attempts. + + + Internal server error {0}. + + + Server did not start as it was already started or disposed. + + + Server started listening. + + + Staring http server for roslyn analysis... + + \ No newline at end of file diff --git a/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj index f82cc646de..2e32f9e3b7 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj +++ b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj @@ -11,6 +11,7 @@ + @@ -22,4 +23,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + From 432c2ade04b9d253d77e1f1fdcbbb0621522b35e Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Fri, 15 Aug 2025 12:20:26 +0200 Subject: [PATCH 04/38] SLVS-2465 Add analysis engine to connect with Roslyn APIs (#6368) [SLVS-2465](https://sonarsource.atlassian.net/browse/SLVS-2465) Part of [SLVS-2465]: https://sonarsource.atlassian.net/browse/SLVS-2465?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../SonarCompositeRuleIdTests.cs | 17 +- src/Core/SonarCompositeRuleId.cs | 4 +- .../DiagnosticDuplicatesComparerTests.cs | 129 +++++++++ .../DiagnosticToRoslynIssueConverterTests.cs | 101 +++++++ .../RoslynFileSemanticAnalysisTests.cs | 106 +++++++ .../Analysis/RoslynFileSyntaxAnalysisTests.cs | 107 +++++++ .../RoslynProjectCompilationProviderTests.cs | 177 ++++++++++++ ...lynSolutionAnalysisCommandProviderTests.cs | 197 +++++++++++++ .../SequentialRoslynAnalysisEngineTests.cs | 261 ++++++++++++++++++ .../Wrappers/RoslynWorkspaceWrapperTests.cs | 32 +++ .../RoslynAnalyzerServer.UnitTests.csproj | 6 + .../packages.lock.json | 45 +++ .../Analysis/DiagnosticDuplicatesComparer.cs | 70 +++++ .../IDiagnosticToRoslynIssueConverter.cs | 29 ++ .../Analysis/IRoslynAnalysisCommand.cs | 30 ++ .../Analysis/IRoslynAnalysisEngine.cs | 32 +++ .../IRoslynProjectCompilationProvider.cs | 33 +++ .../IRoslynSolutionAnalysisCommandProvider.cs | 26 ++ .../Analysis/RoslynAnalysisConfiguration.cs | 30 ++ .../Analysis/RoslynFileSemanticAnalysis.cs | 43 +++ .../Analysis/RoslynFileSyntaxAnalysis.cs | 43 +++ .../Analysis/RoslynIssue.cs | 57 ++++ .../Analysis/RoslynProjectAnalysisRequest.cs | 29 ++ .../RoslynProjectCompilationProvider.cs | 88 ++++++ .../RoslynSolutionAnalysisCommandProvider.cs | 82 ++++++ .../SequentialRoslynAnalysisEngine.cs | 66 +++++ .../SonarRoslynDiagnosticsConverter.cs | 52 ++++ .../IRoslynCompilationWithAnalyzersWrapper.cs | 36 +++ .../Wrappers/IRoslynCompilationWrapper.cs | 36 +++ .../Wrappers/IRoslynProjectWrapper.cs | 39 +++ .../Wrappers/IRoslynSolutionWrapper.cs | 26 ++ .../Wrappers/IRoslynWorkspaceWrapper.cs | 26 ++ .../RoslynCompilationWithAnalyzersWrapper.cs | 43 +++ .../Wrappers/RoslynCompilationWrapper.cs | 45 +++ .../Analysis/Wrappers/RoslynProjectWrapper.cs | 53 ++++ .../Wrappers/RoslynSolutionWrapper.cs | 30 ++ .../Wrappers/RoslynWorkspaceWrapper.cs | 35 +++ .../RoslynAnalyzerServer.csproj | 4 +- src/RoslynAnalyzerServer/packages.lock.json | 236 ++++++++++++++++ 39 files changed, 2495 insertions(+), 6 deletions(-) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs diff --git a/src/Core.UnitTests/SonarCompositeRuleIdTests.cs b/src/Core.UnitTests/SonarCompositeRuleIdTests.cs index cfd6fc8d2a..20ad1629dc 100644 --- a/src/Core.UnitTests/SonarCompositeRuleIdTests.cs +++ b/src/Core.UnitTests/SonarCompositeRuleIdTests.cs @@ -18,10 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace SonarLint.VisualStudio.Core.UnitTests { [TestClass] @@ -85,7 +81,20 @@ public void ToString_ReturnsExpected() output.ToString().Should().Be(input); } + [TestMethod] + [DataRow("repo1", "rule1", "repo1:rule1")] + [DataRow("csharpsquid", "S1234", "csharpsquid:S1234")] + [DataRow("javascript", "S5678", "javascript:S5678")] + [DataRow("my repo", "my rule", "my repo:my rule")] + public void GetFullErrorCode_ConcatenatesWithColon(string repoKey, string ruleKey, string expected) + { + var result = SonarCompositeRuleId.GetFullErrorCode(repoKey, ruleKey); + + result.Should().Be(expected); + } + public static object[][] ReposAndLanguages => LanguageProvider.Instance.AllKnownLanguages.Select(x => new object[] { x.RepoInfo.Key, x }).ToArray(); + [DataTestMethod] [DynamicData(nameof(ReposAndLanguages))] public void Language_MatchesBasedOnRepoKey(string repoKey, Language language) diff --git a/src/Core/SonarCompositeRuleId.cs b/src/Core/SonarCompositeRuleId.cs index 9fee268340..608b5e5f07 100644 --- a/src/Core/SonarCompositeRuleId.cs +++ b/src/Core/SonarCompositeRuleId.cs @@ -47,10 +47,12 @@ public SonarCompositeRuleId(string repoKey, string ruleKey) { RepoKey = repoKey ?? throw new ArgumentNullException(nameof(repoKey)); RuleKey = ruleKey ?? throw new ArgumentNullException(nameof(ruleKey)); - ErrorListErrorCode = repoKey + Separator + ruleKey; + ErrorListErrorCode = GetFullErrorCode(repoKey, ruleKey); Language = LanguageProvider.Instance.AllKnownLanguages.FirstOrDefault(x => x.RepoInfo.Key == repoKey) ?? Language.Unknown; } + public static string GetFullErrorCode(string repoKey, string ruleKey) => repoKey + Separator + ruleKey; + public string ErrorListErrorCode { get; } public string RepoKey { get; } public string RuleKey { get; } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs new file mode 100644 index 0000000000..8041eca5c4 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs @@ -0,0 +1,129 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class DiagnosticDuplicatesComparerTests +{ + private readonly DiagnosticDuplicatesComparer testSubject = DiagnosticDuplicatesComparer.Instance; + private readonly RoslynIssue diagnostic1 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10); + + [TestMethod] + public void Equals_SameReference_ReturnsTrue() + { + var result = testSubject.Equals(diagnostic1, diagnostic1); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Equals_FirstArgumentNull_ReturnsFalse() + { + var result = testSubject.Equals(null, diagnostic1); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Equals_SecondArgumentNull_ReturnsFalse() + { + var result = testSubject.Equals(diagnostic1, null); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Equals_SameRuleKeyAndLocation_ReturnsTrue() + { + var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10); + + var result = testSubject.Equals(diagnostic1, diagnostic2); + + result.Should().BeTrue(); + } + + + [TestMethod] + public void Equals_SameRuleKeyAndLocation_MessageIsIgnored() + { + var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10, "some different message"); + + var result = testSubject.Equals(diagnostic1, diagnostic2); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow("rule2", "file1.cs", 1, 1, 1, 10, DisplayName = "Different RuleKey")] + [DataRow("rule1", "file2.cs", 1, 1, 1, 10, DisplayName = "Different FilePath")] + [DataRow("rule1", "file1.cs", 2, 1, 1, 10, DisplayName = "Different StartLine")] + [DataRow("rule1", "file1.cs", 1, 1, 2, 10, DisplayName = "Different EndLine")] + [DataRow("rule1", "file1.cs", 1, 2, 1, 10, DisplayName = "Different StartLineOffset")] + [DataRow("rule1", "file1.cs", 1, 1, 1, 11, DisplayName = "Different EndLineOffset")] + public void Equals_DifferentValues_ReturnsFalse(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset) + { + var diagnostic2 = CreateDiagnostic(ruleKey, filePath, startLine, startLineOffset, endLine, endLineOffset); + + var result = testSubject.Equals(diagnostic1, diagnostic2); + + result.Should().BeFalse(); + } + + [TestMethod] + public void GetHashCode_SameObjects_ReturnsSameHashCode() + { + var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10); + + var hash1 = testSubject.GetHashCode(diagnostic1); + var hash2 = testSubject.GetHashCode(diagnostic2); + + hash1.Should().Be(hash2); + } + + [TestMethod] + public void GetHashCode_DifferentObjects_ReturnsDifferentHashCodes() + { + var diagnostic2 = CreateDiagnostic("rule2", "file2.cs", 2, 2, 2, 20); + + var hash1 = testSubject.GetHashCode(diagnostic1); + var hash2 = testSubject.GetHashCode(diagnostic2); + + hash1.Should().NotBe(hash2); + } + + [TestMethod] + public void Instance_ReturnsSingletonInstance() + { + var instance1 = DiagnosticDuplicatesComparer.Instance; + var instance2 = DiagnosticDuplicatesComparer.Instance; + + instance1.Should().BeSameAs(instance2); + } + + private static RoslynIssue CreateDiagnostic(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset, string? message = null) + { + var textRange = new RoslynIssueTextRange(startLine, endLine, startLineOffset, endLineOffset); + var location = new RoslynIssueLocation(message ?? "message", filePath, textRange); + return new RoslynIssue(ruleKey, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs new file mode 100644 index 0000000000..77a0d19008 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs @@ -0,0 +1,101 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class DiagnosticToRoslynIssueConverterTests +{ + private readonly DiagnosticToRoslynIssueConverter testSubject = new(); + + [TestMethod] + public void MefCtor_CheckExports() => + MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + public static object[][] TestData => + [ + [Language.CSharp, "S1234", "test message", "c:\\test\\file.cs", 0, 0, 3, 4], + [Language.CSharp, "S1234", "test message", "c:\\test\\file.cs", 1, 1, 3, 4], + [Language.CSharp, "S5678", "multi-line issue", "c:\\test\\file2.cs", 5, 10, 15, 20], + [Language.VBNET, "S1234", "test message", "c:\\test\\file.vb", 0, 0, 3, 4] + ]; + + [DataTestMethod] + [DynamicData(nameof(TestData))] + public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( + Language language, string ruleId, string message, string filePath, + int startLine, int endLine, int startChar, int endChar) + { + var diagnostic = CreateDiagnostic(ruleId, message, filePath, startLine, endLine, startChar, endChar); + var expectedTextRange = new RoslynIssueTextRange( + startLine + 1, // Convert to 1-based + endLine + 1, // Convert to 1-based + startChar, + endChar); + var expectedLocation = new RoslynIssueLocation( + message, + filePath, + expectedTextRange); + var expectedRuleId = $"{language.RepoInfo.Key}:{ruleId}"; + var expectedDiagnostic = new RoslynIssue( + expectedRuleId, + expectedLocation); + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, language); + + result.Should().BeEquivalentTo(expectedDiagnostic); + } + + private static Diagnostic CreateDiagnostic(string id, string message, string filePath, int startLine, int endLine, int startChar, int endChar) + { + var descriptor = new DiagnosticDescriptor( + id, + "Any Title", + message, + "Any Category", + default, + default); + + var location = CreateLocation(filePath, startLine, endLine, startChar, endChar); + + return Diagnostic.Create(descriptor, location); + } + + private static Location CreateLocation(string filePath, int startLine, int endLine, int startChar, int endChar) + { + var textSpan = new TextSpan(12, 34); + var syntaxTree = Substitute.For(); + var linePositionSpan = new LinePositionSpan( + new LinePosition(startLine, startChar), + new LinePosition(endLine, endChar)); + syntaxTree.GetMappedLineSpan(textSpan, CancellationToken.None).Returns(new FileLinePositionSpan(filePath, linePositionSpan)); + + return Location.Create(syntaxTree, textSpan); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs new file mode 100644 index 0000000000..7e07267482 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSemanticAnalysisTests.cs @@ -0,0 +1,106 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.ReturnsExtensions; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynFileSemanticAnalysisTests +{ + private const string TestFilePath = "c:\\test\\file.cs"; + private IRoslynCompilationWithAnalyzersWrapper compilationWrapper = null!; + private TestLogger testLogger = null!; + private RoslynFileSemanticAnalysis testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + testLogger = Substitute.ForPartsOf(); + compilationWrapper = Substitute.For(); + + testSubject = new RoslynFileSemanticAnalysis(TestFilePath, testLogger); + } + + [TestMethod] + public void MefCtor() => + testSubject.AnalysisFilePath.Should().Be(TestFilePath); + + [TestMethod] + public async Task ExecuteAsync_SemanticModelIsNull_ReturnsEmptyCollection() + { + compilationWrapper.GetSemanticModel(TestFilePath).ReturnsNull(); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task ExecuteAsync_SemanticModelExists_ReturnsAnalyzerDiagnostics() + { + var semanticModel = Substitute.For(); + var expectedDiagnostics = ImmutableArray.Create(CreateTestDiagnostic("id1"), CreateTestDiagnostic("id2"), CreateTestDiagnostic("id3")); + compilationWrapper.GetSemanticModel(TestFilePath).Returns(semanticModel); + compilationWrapper.GetAnalyzerSemanticDiagnosticsAsync(semanticModel, CancellationToken.None) + .Returns(expectedDiagnostics); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().BeEquivalentTo(expectedDiagnostics); + } + + [TestMethod] + public async Task ExecuteAsync_CancellationTokenPassed_UsesTokenForDiagnostics() + { + var semanticModel = Substitute.For(); + var cancellationToken = new CancellationToken(true); + compilationWrapper.GetSemanticModel(TestFilePath).Returns(semanticModel); + + await testSubject.ExecuteAsync(compilationWrapper, cancellationToken); + + await compilationWrapper.Received(1).GetAnalyzerSemanticDiagnosticsAsync(semanticModel, cancellationToken); + } + + private static Diagnostic CreateTestDiagnostic(string id) + { + var descriptor = new DiagnosticDescriptor( + id, + "title", + "message", + "category", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "test.cs", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs new file mode 100644 index 0000000000..15e87c98c1 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynFileSyntaxAnalysisTests.cs @@ -0,0 +1,107 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.ReturnsExtensions; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynFileSyntaxAnalysisTests +{ + private const string TestFilePath = "test.cs"; + + private IRoslynCompilationWithAnalyzersWrapper compilationWrapper = null!; + private SyntaxTree syntaxTree = null!; + private TestLogger testLogger = null!; + private RoslynFileSyntaxAnalysis testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + testLogger = Substitute.ForPartsOf(); + compilationWrapper = Substitute.For(); + syntaxTree = Substitute.For(); + + testSubject = new RoslynFileSyntaxAnalysis(TestFilePath, testLogger); + } + + [TestMethod] + public void Constructor_SetsProperties() => + testSubject.AnalysisFilePath.Should().Be(TestFilePath); + + [TestMethod] + public async Task ExecuteAsync_SyntaxTreeExists_ReturnsSyntaxDiagnostics() + { + var expectedDiagnostics = ImmutableArray.Create(CreateTestDiagnostic("id1"), CreateTestDiagnostic("id2")); + compilationWrapper.GetSyntaxTree(TestFilePath).Returns(syntaxTree); + compilationWrapper.GetAnalyzerSyntaxDiagnosticsAsync(syntaxTree, Arg.Any()).Returns(expectedDiagnostics); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().BeEquivalentTo(expectedDiagnostics); + } + + [TestMethod] + public async Task ExecuteAsync_NoSyntaxTree_ReturnsEmptyArray() + { + compilationWrapper.GetSyntaxTree(TestFilePath).ReturnsNull(); + + var result = await testSubject.ExecuteAsync(compilationWrapper, CancellationToken.None); + + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task ExecuteAsync_CancellationTokenPassed_UsesTokenForDiagnostics() + { + var expectedToken = new CancellationToken(true); + compilationWrapper.GetSyntaxTree(TestFilePath).Returns(syntaxTree); + + await testSubject.ExecuteAsync(compilationWrapper, expectedToken); + + await compilationWrapper.Received(1).GetAnalyzerSyntaxDiagnosticsAsync( + syntaxTree, + Arg.Is(token => token.Equals(expectedToken))); + } + + private static Diagnostic CreateTestDiagnostic(string id) + { + var descriptor = new DiagnosticDescriptor( + id, + "title", + "message", + "category", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "test.cs", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs new file mode 100644 index 0000000000..c7770e77ff --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs @@ -0,0 +1,177 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynProjectCompilationProviderTests +{ + private DiagnosticAnalyzer analyzer1 = null!; + private DiagnosticAnalyzer analyzer2 = null!; + private DiagnosticAnalyzer analyzer3 = null!; + private AnalyzerOptions analyzerOptions = null!; + private ImmutableArray analyzers; + private IRoslynCompilationWrapper compilation = null!; + private CompilationOptions compilationOptions = null!; + private IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers = null!; + private ImmutableDictionary configurations = null!; + private ImmutableDictionary diagnosticOptions = null!; + private AdditionalText existingAdditionalFile = null!; + private TestLogger logger = null!; + private IRoslynProjectWrapper project = null!; + private AdditionalText sonarLintXml = null!; + private RoslynProjectCompilationProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + logger = Substitute.ForPartsOf(); + + SetUpCompilation(); + SetUpAdditionalFiles(); + SetUpProject(); + SetUpAnalyzers(); + diagnosticOptions = ImmutableDictionary.Empty + .Add("SomeId", ReportDiagnostic.Warn); + configurations = ImmutableDictionary.Empty + .Add(Language.CSharp, new RoslynAnalysisConfiguration( + sonarLintXml, + diagnosticOptions, + analyzers)); + testSubject = new RoslynProjectCompilationProvider(logger); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task GetProjectCompilationAsync_ConfiguresCompilationWithCorrectOptions() + { + var result = await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); + + result.Should().Be(compilationWithAnalyzers); + compilation.Received(1).WithOptions(Arg.Is(options => + options.SpecificDiagnosticOptions == diagnosticOptions)); + compilation.Received(1).WithAnalyzers( + Arg.Is>(analyzersArg => + analyzersArg.SequenceEqual(analyzers, null as IEqualityComparer)), + Arg.Is(options => + options.Options != null + && options.Options.AdditionalFiles.SequenceEqual(ImmutableArray.Create(existingAdditionalFile, sonarLintXml), null as IEqualityComparer) + && options.ConcurrentAnalysis == true + && options.ReportSuppressedDiagnostics == false + && options.LogAnalyzerExecutionTime == false)); + } + + [TestMethod] + public async Task GetProjectCompilationAsync_RemovesExistingSonarLintXml() + { + var existingSonarLintXml = Substitute.For(); + existingSonarLintXml.Path.Returns(@"c:\some\other\path\SonarLint.xml"); + var analyzerOptionsWithSonarLintXml = new AnalyzerOptions( + ImmutableArray.Create(existingAdditionalFile, existingSonarLintXml)); + project.RoslynAnalyzerOptions.Returns(analyzerOptionsWithSonarLintXml); + compilation.WithAnalyzers(Arg.Any>(), Arg.Any()) + .Returns(compilationWithAnalyzers); + + await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); + + compilation.Received(1).WithAnalyzers( + Arg.Any>(), + Arg.Is(options => + options.Options != null + && options.Options.AdditionalFiles.SequenceEqual(ImmutableArray.Create(existingAdditionalFile, sonarLintXml), null as IEqualityComparer) + && options.ConcurrentAnalysis == true + && options.ReportSuppressedDiagnostics == false + && options.LogAnalyzerExecutionTime == false)); + } + + [TestMethod] + public async Task GetProjectCompilationAsync_AnalyzerException_LogsError() + { + CompilationWithAnalyzersOptions capturedOptions = null!; + compilation.WithAnalyzers( + Arg.Any>(), + Arg.Do(x => capturedOptions = x)) + .Returns(compilationWithAnalyzers); + await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); + capturedOptions.Should().NotBeNull(); + var exception = new InvalidOperationException("test exception"); + var diagnostic = Diagnostic.Create("TestId", "TestCategory", "TestMessage", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 1); + + capturedOptions.OnAnalyzerException!(exception, analyzer1, diagnostic); + + logger.AssertPartialOutputStringExists( + analyzer1.GetType().Name, + "TestId", + "test exception"); + } + + private void SetUpAnalyzers() + { + analyzer1 = Substitute.For(); + analyzer2 = Substitute.For(); + analyzer3 = Substitute.For(); + analyzers = ImmutableArray.Create(analyzer1, analyzer2, analyzer3); + } + + private void SetUpProject() + { + project = Substitute.For(); + analyzerOptions = new AnalyzerOptions(ImmutableArray.Create(existingAdditionalFile)); + project.RoslynAnalyzerOptions.Returns(analyzerOptions); + project.GetCompilationAsync(Arg.Any()).Returns(compilation); + } + + private void SetUpAdditionalFiles() + { + sonarLintXml = Substitute.For(); + sonarLintXml.Path.Returns(@"c:\path\to\SonarLint.xml"); + + existingAdditionalFile = Substitute.For(); + existingAdditionalFile.Path.Returns(@"c:\path\to\existing.txt"); + } + + private void SetUpCompilation() + { + compilation = Substitute.For(); + compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication); + compilation.RoslynCompilationOptions.Returns(compilationOptions); + compilationWithAnalyzers = Substitute.For(); + compilation.Language.Returns(Language.CSharp); + compilation.WithOptions(Arg.Any()).Returns(compilation); + compilation.WithAnalyzers(Arg.Any>(), Arg.Any()) + .Returns(compilationWithAnalyzers); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs new file mode 100644 index 0000000000..204f4a4639 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs @@ -0,0 +1,197 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynSolutionAnalysisCommandProviderTests +{ + private const string File1Cs = "file1.cs"; + private const string File2Cs = "file2.cs"; + private const string File3Cs = "file3.cs"; + private const string File4Cs = "file4.cs"; + private const string AnalyzedFile1Cs = "analyzedFile1.cs"; + private const string AnalyzedFile2Cs = "analyzedFile2.cs"; + private const string AnalyzedFile3Cs = "analyzedFile3.cs"; + private const string Project1 = "project1"; + private const string Project2 = "project2"; + private const string Project3 = "project3"; + private const string Project4 = "project4"; + + private IRoslynWorkspaceWrapper workspaceWrapper = null!; + private TestLogger logger = null!; + private IRoslynSolutionWrapper solutionWrapper = null!; + private RoslynSolutionAnalysisCommandProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + workspaceWrapper = Substitute.For(); + logger = new TestLogger(); + solutionWrapper = Substitute.For(); + workspaceWrapper.GetCurrentSolution().Returns(solutionWrapper); + testSubject = new RoslynSolutionAnalysisCommandProvider(workspaceWrapper, logger); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_NoProjects_ReturnsEmptyList() + { + solutionWrapper.Projects.Returns(new List()); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().BeEmpty(); + logger.AssertPartialOutputStringExists("No projects to analyze"); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_ProjectDoesNotSupportCompilation_SkipsProject() + { + var project1 = CreateProject(Project1, false); + solutionWrapper.Projects.Returns([project1]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().BeEmpty(); + logger.AssertPartialOutputStringExists("Project project1 does not support compilation"); + logger.AssertPartialOutputStringExists("No projects to analyze"); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_ProjectWithNoMatchingFiles_SkipsProject() + { + var project1 = CreateProject(Project1); + project1.ContainsDocument(Arg.Any(), out _).Returns(false); + + solutionWrapper.Projects.Returns([project1]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().BeEmpty(); + logger.AssertPartialOutputStringExists("No projects to analyze"); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_MultipleProjects_OnlyProjectWithFilesIsReturned() + { + var project1 = CreateProject(Project1); + project1.ContainsDocument(Arg.Any(), out _).Returns(false); + var project2 = CreateProject(Project2); + SetupContainsDocument(project2, File1Cs, AnalyzedFile1Cs); + solutionWrapper.Projects.Returns([project1, project2]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs]); + + result.Should().ContainSingle(); + var command = result.Single(); + command.Project.Should().Be(project2); + command.AnalysisCommands.OfType().Should().HaveCount(1); + command.AnalysisCommands.OfType().Should().HaveCount(1); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_MultipleMatchingFiles_AllFilesIncluded() + { + var project = CreateProject(Project1); + SetupContainsDocument(project, File1Cs, AnalyzedFile1Cs); + SetupContainsDocument(project, File2Cs, AnalyzedFile2Cs); + project.ContainsDocument(File3Cs, out _).Returns(false); + solutionWrapper.Projects.Returns([project]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs, File2Cs, File3Cs]); + + var command = result.Single(); + command.Project.Should().Be(project); + command.AnalysisCommands.Should().HaveCount(4); + ValidateContainsAllTypesOfAnalysisForFile(command, AnalyzedFile1Cs); + ValidateContainsAllTypesOfAnalysisForFile(command, AnalyzedFile2Cs); + } + + [TestMethod] + public void GetAnalysisCommandsForCurrentSolution_MixedProjectResults_ReturnsCorrectProjects() + { + var projectWithNoCompilation = CreateProject(Project1, false); + var projectWithNofiles = CreateProject(Project2); + projectWithNofiles.ContainsDocument(Arg.Any(), out _).Returns(false); + var project3 = CreateProject(Project3); + SetupContainsDocument(project3, File1Cs, AnalyzedFile1Cs); + SetupContainsDocument(project3, File2Cs, AnalyzedFile2Cs); + var project4 = CreateProject(Project4); + SetupContainsDocument(project4, File3Cs, AnalyzedFile3Cs); + SetupContainsDocument(project4, File1Cs, AnalyzedFile1Cs); + solutionWrapper.Projects.Returns([projectWithNoCompilation, projectWithNofiles, project3, project4]); + + var result = testSubject.GetAnalysisCommandsForCurrentSolution([File1Cs, File2Cs, File3Cs, File4Cs]); + + result.Should().HaveCount(2); + result[0].Project.Should().Be(project3); + result[0].AnalysisCommands.Should().HaveCount(4); + ValidateContainsAllTypesOfAnalysisForFile(result[0], AnalyzedFile1Cs); + ValidateContainsAllTypesOfAnalysisForFile(result[0], AnalyzedFile2Cs); + result[1].Project.Should().Be(project4); + result[1].AnalysisCommands.Should().HaveCount(4); + ValidateContainsAllTypesOfAnalysisForFile(result[1], AnalyzedFile3Cs); + ValidateContainsAllTypesOfAnalysisForFile(result[1], AnalyzedFile1Cs); + logger.AssertPartialOutputStringExists("Project project1 does not support compilation"); + } + + private void ValidateContainsAllTypesOfAnalysisForFile(RoslynProjectAnalysisRequest request, string analysisFilePath) + { + ValidateContainsSyntacticAnalysisForFile(request, analysisFilePath); + ValidateContainsSemanticAnalysisForFile(request, analysisFilePath); + } + + private void ValidateContainsSyntacticAnalysisForFile(RoslynProjectAnalysisRequest request, string analysisFilePath) => + request.AnalysisCommands.Any(x => x is RoslynFileSyntaxAnalysis semanticAnalysis && semanticAnalysis.AnalysisFilePath == analysisFilePath).Should().BeTrue(); + + private void ValidateContainsSemanticAnalysisForFile(RoslynProjectAnalysisRequest request, string analysisFilePath) => + request.AnalysisCommands.Any(x => x is RoslynFileSemanticAnalysis semanticAnalysis && semanticAnalysis.AnalysisFilePath == analysisFilePath).Should().BeTrue(); + + + private static IRoslynProjectWrapper CreateProject(string projectName, bool supportsCompilation = true) + { + var project = Substitute.For(); + project.Name.Returns(projectName); + project.SupportsCompilation.Returns(supportsCompilation); + return project; + } + + private static void SetupContainsDocument(IRoslynProjectWrapper project, string file, string analyzedFile) => + project.ContainsDocument(file, out Arg.Any()).Returns(x => + { + x[1] = analyzedFile; + return true; + }); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs new file mode 100644 index 0000000000..70407dfea9 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs @@ -0,0 +1,261 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; +using Language = SonarLint.VisualStudio.Core.Language; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class SequentialRoslynAnalysisEngineTests +{ + private IDiagnosticToRoslynIssueConverter issueConverter = null!; + private IRoslynProjectCompilationProvider projectCompilationProvider = null!; + private TestLogger logger = null!; + private ImmutableDictionary configurations = null!; + private CancellationToken cancellationToken; + private SequentialRoslynAnalysisEngine testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + issueConverter = Substitute.For(); + projectCompilationProvider = Substitute.For(); + logger = Substitute.ForPartsOf(); + + testSubject = new SequentialRoslynAnalysisEngine(issueConverter, projectCompilationProvider, logger); + + configurations = ImmutableDictionary.Create(); + cancellationToken = new CancellationToken(); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task AnalyzeAsync_EmptyAnalysisCommands_ReturnsEmptyCollection() + { + var result = await testSubject.AnalyzeAsync([], configurations, cancellationToken); + + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task AnalyzeAsync_SingleProjectWithNoAnalysisCommands_ReturnsEmptyCollection() + { + var (project, _) = SetupProjectAnalysisRequestAndCompilation(); + + var result = await testSubject.AnalyzeAsync([CreateProjectRequest(project)], configurations, cancellationToken); + + result.Should().BeEmpty(); + await projectCompilationProvider.Received(1).GetProjectCompilationAsync(project, configurations, cancellationToken); + } + + public static object[][] RoslynLanguages => + [ + [Language.CSharp], + [Language.VBNET] + ]; + + [DataTestMethod] + [DynamicData(nameof(RoslynLanguages))] + public async Task AnalyzeAsync_SingleProjectWithSingleAnalysisCommand_ReturnsDiagnostics(Language language) + { + var (diagnostic, roslynIssue) = SetUpDiagnosticAndConvertedModel("test-rule", "test message"); + var (requestForProject, compilationForProject) = SetupProjectAnalysisRequestAndCompilation([[diagnostic]]); + compilationForProject.Language.Returns(language); + + var result = await testSubject.AnalyzeAsync([requestForProject], configurations, cancellationToken); + + result.Should().BeEquivalentTo(roslynIssue); + VerifyAnalysisExecution(requestForProject, compilationForProject, [diagnostic], language); + } + + [TestMethod] + public async Task AnalyzeAsync_DuplicateDiagnostics_ReturnsSingleDiagnostic() + { + var (duplicateDiagnostic1, duplicateIssue1) = SetUpDiagnosticAndConvertedModel("test-rule", "test message"); + var (duplicateDiagnostic2, duplicateIssue2) = SetUpDiagnosticAndConvertedModel("test-rule", "test message duplicate"); + var (requestForProject, compilationForProject) = SetupProjectAnalysisRequestAndCompilation([[duplicateDiagnostic1], [duplicateDiagnostic2]]); + + var result = await testSubject.AnalyzeAsync([requestForProject], configurations, cancellationToken); + + result.Should().BeEquivalentTo(duplicateIssue1); + VerifyAnalysisExecution(requestForProject, compilationForProject, [duplicateDiagnostic1, duplicateDiagnostic2]); + logger.AssertPartialOutputStringExists( + $"Duplicate diagnostic discarded ID: {duplicateIssue2.RuleKey}, File: {duplicateIssue2.PrimaryLocation.FilePath}, Line: {duplicateIssue2.PrimaryLocation.TextRange.StartLine}"); + } + + [TestMethod] + public async Task AnalyzeAsync_DuplicateDiagnosticsInDifferentProjects_ReturnsSingleDiagnostic() + { + var (diagnostic1, duplicateIssue) = SetUpDiagnosticAndConvertedModel("test-rule", "test message"); + var (requestForProject1, compilationForProject1) = SetupProjectAnalysisRequestAndCompilation([[diagnostic1]]); + var (diagnostic2, _) = SetUpDiagnosticAndConvertedModel("test-rule-duplicate", "test message duplicate", duplicateIssue); + var (requestForProject2, compilationForProject2) = SetupProjectAnalysisRequestAndCompilation([[diagnostic2]]); + + var result = await testSubject.AnalyzeAsync([requestForProject1, requestForProject2], configurations, cancellationToken); + + result.Should().BeEquivalentTo(duplicateIssue); + VerifyAnalysisExecution(requestForProject1, compilationForProject1, [diagnostic1]); + VerifyAnalysisExecution(requestForProject2, compilationForProject2, [diagnostic2]); + logger.AssertPartialOutputStringExists( + $"Duplicate diagnostic discarded ID: {duplicateIssue.RuleKey}, File: {duplicateIssue.PrimaryLocation.FilePath}, Line: {duplicateIssue.PrimaryLocation.TextRange.StartLine}"); + } + + [TestMethod] + public async Task AnalyzeAsync_MultipleProjects_ProcessesAllProjects() + { + var (diagnostic1, sonarIssue1) = SetUpDiagnosticAndConvertedModel("rule1", "message1"); + var (requestForProject1, compilationForProject1) = SetupProjectAnalysisRequestAndCompilation([[diagnostic1]]); + var (diagnostic2, sonarIssue2) = SetUpDiagnosticAndConvertedModel("rule2", "message2"); + var (requestForProject2, compilationForProject2) = SetupProjectAnalysisRequestAndCompilation([[diagnostic2]]); + + var result = await testSubject.AnalyzeAsync([requestForProject1, requestForProject2], configurations, cancellationToken); + + result.Should().BeEquivalentTo([sonarIssue1, sonarIssue2]); + VerifyAnalysisExecution(requestForProject1, compilationForProject1, [diagnostic1]); + VerifyAnalysisExecution(requestForProject2, compilationForProject2, [diagnostic2]); + } + + [TestMethod] + public async Task AnalyzeAsync_SingleProjectWithMultipleCommands_ReturnsAllDiagnostics() + { + var (diagnostic1A, sonarIssue1A) = SetUpDiagnosticAndConvertedModel("rule1", "message1"); + var (diagnostic1B, sonarIssue1B) = SetUpDiagnosticAndConvertedModel("rule2", "message2"); + var (diagnostic2A, sonarIssue2A) = SetUpDiagnosticAndConvertedModel("rule3", "message3"); + var (diagnostic2B, sonarIssue2B) = SetUpDiagnosticAndConvertedModel("rule4", "message4"); + var (requestForProject, compilationForProject) = SetupProjectAnalysisRequestAndCompilation([[diagnostic1A, diagnostic1B], [diagnostic2A, diagnostic2B]]); + + var result = await testSubject.AnalyzeAsync([requestForProject], configurations, cancellationToken); + + result.Should().BeEquivalentTo(sonarIssue1A, sonarIssue1B, sonarIssue2A, sonarIssue2B); + VerifyAnalysisExecution(requestForProject, compilationForProject, [diagnostic1A, diagnostic1B, diagnostic2A, diagnostic2B]); + } + + private (RoslynProjectAnalysisRequest request, IRoslynCompilationWithAnalyzersWrapper compilation) SetupProjectAnalysisRequestAndCompilation( + Diagnostic[][] diagnosticsPerCommand) + { + var (project, projectCompilation) = SetupProjectAnalysisRequestAndCompilation(); + var analysisCommands = diagnosticsPerCommand.Select(x => SetupCommandWithDiagnostics(projectCompilation, x)).ToArray(); + + return (new RoslynProjectAnalysisRequest(project, analysisCommands), projectCompilation); + } + + private RoslynProjectAnalysisRequest CreateProjectRequest(IRoslynProjectWrapper project, params IRoslynAnalysisCommand[] commands) => + new(project, commands); + + private (IRoslynProjectWrapper project, IRoslynCompilationWithAnalyzersWrapper projectCompilation) SetupProjectAnalysisRequestAndCompilation() + { + var project = Substitute.For(); + var compilation = SetupCompilation(project); + + return (project, compilation); + } + + private IRoslynAnalysisCommand SetupCommandWithDiagnostics( + IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, + params Diagnostic[] diagnostics) + { + var command = Substitute.For(); + command.ExecuteAsync(compilationWithAnalyzers, CancellationToken.None) + .Returns(ImmutableArray.Create(diagnostics)); + return command; + } + + private (Diagnostic, RoslynIssue) SetUpDiagnosticAndConvertedModel( + string ruleId, + string message, + RoslynIssue? existingSonarIssue = null) + { + var diagnostic = CreateTestDiagnostic(ruleId, message); + + var sonarIssue = existingSonarIssue ?? CreateSonarIssue(ruleId, message); + issueConverter.ConvertToSonarDiagnostic(diagnostic, Arg.Any()).Returns(sonarIssue); + + return (diagnostic, sonarIssue); + } + + private IRoslynCompilationWithAnalyzersWrapper SetupCompilation(IRoslynProjectWrapper project) + { + var compilationWithAnalyzers = Substitute.For(); + projectCompilationProvider.GetProjectCompilationAsync(project, configurations, cancellationToken) + .Returns(compilationWithAnalyzers); + return compilationWithAnalyzers; + } + + private void VerifyAnalysisExecution( + RoslynProjectAnalysisRequest projectRequest, + IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, + Diagnostic[] diagnostics, + Language? language = null) + + { + projectCompilationProvider.Received(1) + .GetProjectCompilationAsync(projectRequest.Project, configurations, cancellationToken).IgnoreAwaitForAssert(); + foreach (var analysisCommand in projectRequest.AnalysisCommands) + { + analysisCommand.Received(1).ExecuteAsync(compilationWithAnalyzers, cancellationToken).IgnoreAwaitForAssert(); + } + foreach (var diagnostic in diagnostics) + { + issueConverter.Received(1).ConvertToSonarDiagnostic(diagnostic, language ?? Arg.Any()); + } + } + + private static Diagnostic CreateTestDiagnostic(string id, string message) + { + var descriptor = new DiagnosticDescriptor( + id, + "title", + message, + "category", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "test.cs", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } + + private static RoslynIssue CreateSonarIssue(string ruleId, string message) + { + var textRange = new RoslynIssueTextRange(1, 1, 0, 1); + var location = new RoslynIssueLocation(message, "test.cs", textRange); + return new RoslynIssue(ruleId, location); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs new file mode 100644 index 0000000000..fe31c6ab98 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/RoslynWorkspaceWrapperTests.cs @@ -0,0 +1,32 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Wrappers; + +[TestClass] +public class RoslynWorkspaceWrapperTests +{ + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsSingletonMefComponent(); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj index ca5cfeed40..565b4d9b43 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj @@ -6,6 +6,7 @@ SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests enable + enable @@ -13,4 +14,9 @@ + + + + + diff --git a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json index af99b4f6ad..4d06016b56 100644 --- a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json +++ b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json @@ -14,6 +14,30 @@ "resolved": "0.11.4", "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" }, + "Microsoft.CodeAnalysis.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.2", + "System.Collections.Immutable": "5.0.0", + "System.Memory": "4.5.4", + "System.Reflection.Metadata": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "aDRRb7y/sXoJyDqFEQ3Il9jZxyUMHkShzZeCRjQf3SS84n2J0cTEi3TbwVZE9XJvAeMJhGfVVxwOdjYBg6ljmw==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.11.0]" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -162,6 +186,11 @@ "resolved": "16.5.0", "contentHash": "K0hfdWy+0p8DJXxzpNc4T5zHm4hf9QONAvyzvw3utKExmxRBShtV/+uHVYTblZWk+rIHNEHeglyXMmqfSshdFA==" }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "16.6.1", @@ -1049,6 +1078,14 @@ "resolved": "4.6.0", "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", + "dependencies": { + "System.Collections.Immutable": "5.0.0" + } + }, "System.Runtime": { "type": "Transitive", "resolved": "4.3.0", @@ -1102,6 +1139,14 @@ "resolved": "5.0.0", "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, "System.Threading.AccessControl": { "type": "Transitive", "resolved": "5.0.0", diff --git a/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs new file mode 100644 index 0000000000..9482263d9f --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs @@ -0,0 +1,70 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public class DiagnosticDuplicatesComparer : IEqualityComparer +{ + public static DiagnosticDuplicatesComparer Instance { get; } = new(); + + private DiagnosticDuplicatesComparer() + { + } + + public bool Equals(RoslynIssue? x, RoslynIssue? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + if (x is null) + { + return false; + } + if (y is null) + { + return false; + } + + return x.RuleKey == y.RuleKey && LocationEquals(x.PrimaryLocation, y.PrimaryLocation); + } + + public int GetHashCode(RoslynIssue obj) + { + unchecked + { + var hc = obj.RuleKey.GetHashCode(); + const int prime = 397; + hc = (hc * prime) ^ obj.PrimaryLocation.FilePath.GetHashCode(); + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLine; + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLineOffset; + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.EndLine; + hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.EndLineOffset; + return hc; + } + } + + private static bool LocationEquals(RoslynIssueLocation xPrimaryLocation, RoslynIssueLocation yPrimaryLocation) => + xPrimaryLocation.FilePath == yPrimaryLocation.FilePath && + xPrimaryLocation.TextRange.StartLine == yPrimaryLocation.TextRange.StartLine && + xPrimaryLocation.TextRange.EndLine == yPrimaryLocation.TextRange.EndLine && + xPrimaryLocation.TextRange.StartLineOffset == yPrimaryLocation.TextRange.StartLineOffset && + xPrimaryLocation.TextRange.EndLineOffset == yPrimaryLocation.TextRange.EndLineOffset; +} diff --git a/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs b/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs new file mode 100644 index 0000000000..d9c5309c9a --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public interface IDiagnosticToRoslynIssueConverter +{ + RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language language); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs new file mode 100644 index 0000000000..89f9c3271c --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisCommand.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynAnalysisCommand +{ + Task> ExecuteAsync(IRoslynCompilationWithAnalyzersWrapper compilation, CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs new file mode 100644 index 0000000000..ad66174aa9 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs @@ -0,0 +1,32 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynAnalysisEngine +{ + Task> AnalyzeAsync( + List projectsAnalysis, + ImmutableDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs new file mode 100644 index 0000000000..843356d871 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs @@ -0,0 +1,33 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynProjectCompilationProvider +{ + Task GetProjectCompilationAsync( + IRoslynProjectWrapper project, + ImmutableDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs new file mode 100644 index 0000000000..da38cb34ab --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynSolutionAnalysisCommandProvider.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynSolutionAnalysisCommandProvider +{ + List GetAnalysisCommandsForCurrentSolution(string[] filePaths); +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs new file mode 100644 index 0000000000..1e28060fe4 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public record RoslynAnalysisConfiguration( + AdditionalText SonarLintXml, + ImmutableDictionary DiagnosticOptions, + ImmutableArray Analyzers); diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs b/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs new file mode 100644 index 0000000000..b6e3a46d25 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs @@ -0,0 +1,43 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal class RoslynFileSemanticAnalysis(string analysisFilePath, ILogger logger) : IRoslynAnalysisCommand +{ + public string AnalysisFilePath { get; } = analysisFilePath; + + public async Task> ExecuteAsync(IRoslynCompilationWithAnalyzersWrapper compilation, CancellationToken token) + { + var semanticModel = compilation.GetSemanticModel(AnalysisFilePath); + if (semanticModel == null) + { + logger.LogVerbose("No semantic model found for {0}", AnalysisFilePath); + return ImmutableArray.Empty; + } + + return await compilation.GetAnalyzerSemanticDiagnosticsAsync(semanticModel, token); + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs b/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs new file mode 100644 index 0000000000..a40b20cbda --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs @@ -0,0 +1,43 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal class RoslynFileSyntaxAnalysis(string analysisFilePath, ILogger logger) : IRoslynAnalysisCommand +{ + public string AnalysisFilePath { get; } = analysisFilePath; + + public async Task> ExecuteAsync(IRoslynCompilationWithAnalyzersWrapper compilation, CancellationToken token) + { + var syntaxTree = compilation.GetSyntaxTree(AnalysisFilePath); + if (syntaxTree == null) + { + logger.LogVerbose("No syntax tree found for {0}", AnalysisFilePath); + return ImmutableArray.Empty; + } + + return await compilation.GetAnalyzerSyntaxDiagnosticsAsync(syntaxTree, token); + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs new file mode 100644 index 0000000000..77696e3bc1 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs @@ -0,0 +1,57 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +public class RoslynIssue( + string ruleKey, + RoslynIssueLocation primaryLocation, + IReadOnlyList? flows = null) +{ + private static readonly IReadOnlyList EmptyFlows = []; + + public string RuleKey { get; } = ruleKey; + public RoslynIssueLocation PrimaryLocation { get; } = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); + public IReadOnlyList Flows { get; } = flows ?? EmptyFlows; +} + +public class RoslynIssueFlow(IReadOnlyList locations) +{ + public IReadOnlyList Locations { get; } = locations ?? throw new ArgumentNullException(nameof(locations)); +} + +public class RoslynIssueLocation(string message, string filePath, RoslynIssueTextRange textRange) +{ + public string FilePath { get; } = filePath; + public string Message { get; } = message; + public RoslynIssueTextRange TextRange { get; } = textRange; +} + +public class RoslynIssueTextRange( + int startLine, + int endLine, + int startLineOffset, + int endLineOffset) +{ + public int StartLine { get; } = startLine; + public int EndLine { get; } = endLine; + public int StartLineOffset { get; } = startLineOffset; + public int EndLineOffset { get; } = endLineOffset; +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs new file mode 100644 index 0000000000..646b7a2a18 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectAnalysisRequest.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal class RoslynProjectAnalysisRequest(IRoslynProjectWrapper project, IReadOnlyCollection analysisCommands) +{ + public IRoslynProjectWrapper Project { get; } = project; + public IReadOnlyCollection AnalysisCommands { get; } = analysisCommands; +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs new file mode 100644 index 0000000000..2c0f5ef50d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs @@ -0,0 +1,88 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynProjectCompilationProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynProjectCompilationProvider(ILogger logger) : IRoslynProjectCompilationProvider +{ + private readonly ILogger analyzerExceptionLogger = logger.ForContext("Roslyn Analysis", "Analyzer Exception"); + + public async Task GetProjectCompilationAsync( + IRoslynProjectWrapper project, + ImmutableDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token) + { + var compilation = await project.GetCompilationAsync(token); + + var analysisConfigurationForLanguage = sonarRoslynAnalysisConfigurations[compilation.Language]; + + return ApplyAnalyzersAndAdditionalFile( + ApplyDiagnosticOptions(compilation, analysisConfigurationForLanguage), + project, + analysisConfigurationForLanguage); + } + + private IRoslynCompilationWithAnalyzersWrapper ApplyAnalyzersAndAdditionalFile( + IRoslynCompilationWrapper compilation, + IRoslynProjectWrapper project, + RoslynAnalysisConfiguration analysisConfigurationForLanguage) + { + var additionalFiles = project.RoslynAnalyzerOptions.AdditionalFiles; + var sonarLintXmlName = Path.GetFileName(analysisConfigurationForLanguage.SonarLintXml.Path); + var analyzerOptions = project.RoslynAnalyzerOptions.WithAdditionalFiles(additionalFiles + .Where(x => Path.GetFileName(x.Path) != sonarLintXmlName) + .Concat([analysisConfigurationForLanguage.SonarLintXml]) + .ToImmutableArray()); + + var compilationWithAnalyzersOptions = new CompilationWithAnalyzersOptions( + analyzerOptions, + OnAnalyzerException, + true, + false, + false); + + return compilation + .WithAnalyzers(analysisConfigurationForLanguage.Analyzers, compilationWithAnalyzersOptions); + } + + private static IRoslynCompilationWrapper ApplyDiagnosticOptions( + IRoslynCompilationWrapper compilation, + RoslynAnalysisConfiguration analysisConfigurationForLanguage) + { + var compilationOptions = compilation.RoslynCompilationOptions.WithSpecificDiagnosticOptions(analysisConfigurationForLanguage.DiagnosticOptions); + return compilation.WithOptions(compilationOptions); + } + + private void OnAnalyzerException(Exception arg1, DiagnosticAnalyzer arg2, Diagnostic arg3) => + analyzerExceptionLogger.LogVerbose( + new MessageLevelContext { VerboseContext = [arg2.GetType().Name, arg3.Id] }, + arg1.ToString()); +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs new file mode 100644 index 0000000000..f66cff85bf --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs @@ -0,0 +1,82 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynSolutionAnalysisCommandProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynSolutionAnalysisCommandProvider( + IRoslynWorkspaceWrapper roslynWorkspaceWrapper, + ILogger logger) : IRoslynSolutionAnalysisCommandProvider +{ + private readonly ILogger logger = logger.ForContext("Roslyn Analysis", "Configuration"); + + public List GetAnalysisCommandsForCurrentSolution(string[] filePaths) + { + var result = new List(); + + var solution = roslynWorkspaceWrapper.GetCurrentSolution(); + + foreach (var project in solution.Projects) + { + if (!project.SupportsCompilation) + { + logger.LogVerbose("Project {0} does not support compilation", project.Name); + continue; + } + + var commands = GetCompilationCommandsForProject(filePaths, project); + + if (commands.Any()) + { + result.Add(new RoslynProjectAnalysisRequest(project, commands)); + } + } + + if (!result.Any()) + { + logger.WriteLine("No projects to analyze"); + } + + return result; + } + + private List GetCompilationCommandsForProject(string[] filePaths, IRoslynProjectWrapper project) + { + var commands = new List(); + + foreach (var filePath in filePaths) + { + if (!project.ContainsDocument(filePath, out var analysisFilePath)) + { + continue; + } + + commands.Add(new RoslynFileSyntaxAnalysis(analysisFilePath, logger)); + commands.Add(new RoslynFileSemanticAnalysis(analysisFilePath, logger)); + } + return commands; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs new file mode 100644 index 0000000000..a1ac25ccf6 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs @@ -0,0 +1,66 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.IO; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynAnalysisEngine))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class SequentialRoslynAnalysisEngine( + IDiagnosticToRoslynIssueConverter issueConverter, + IRoslynProjectCompilationProvider projectCompilationProvider, + ILogger logger) : IRoslynAnalysisEngine +{ + private readonly ILogger logger = logger.ForContext("Roslyn Analysis", "Engine"); + + public async Task> AnalyzeAsync( + List projectsAnalysis, + ImmutableDictionary sonarRoslynAnalysisConfigurations, + CancellationToken token) + { + var uniqueDiagnostics = new HashSet(DiagnosticDuplicatesComparer.Instance); + foreach (var projectAnalysisCommands in projectsAnalysis) + { + var compilationWithAnalyzers = await projectCompilationProvider.GetProjectCompilationAsync(projectAnalysisCommands.Project, sonarRoslynAnalysisConfigurations, token); + + // todo SLVS-2467 issue streaming + foreach (var analysisCommand in projectAnalysisCommands.AnalysisCommands) + { + var issues = await analysisCommand.ExecuteAsync(compilationWithAnalyzers, token); + + foreach (var diagnostic in issues.Select(d => issueConverter.ConvertToSonarDiagnostic(d, compilationWithAnalyzers.Language))) + { + // todo SLVS-2468 improve issue merging + if (!uniqueDiagnostics.Add(diagnostic)) + { + logger.LogVerbose("Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}", diagnostic.RuleKey, Path.GetFileName(diagnostic.PrimaryLocation.FilePath), diagnostic.PrimaryLocation.TextRange.StartLine); + } + } + } + } + + return uniqueDiagnostics; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs b/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs new file mode 100644 index 0000000000..bcfa9aeef9 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs @@ -0,0 +1,52 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IDiagnosticToRoslynIssueConverter))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class DiagnosticToRoslynIssueConverter : IDiagnosticToRoslynIssueConverter +{ + public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language language) + { + var fileLinePositionSpan = diagnostic.Location.GetMappedLineSpan(); + + var textRange = new RoslynIssueTextRange( + fileLinePositionSpan.StartLinePosition.Line + 1, // roslyn lines are 0-based, while we use 1-based + fileLinePositionSpan.EndLinePosition.Line + 1, // roslyn lines are 0-based, while we use 1-based + fileLinePositionSpan.StartLinePosition.Character, + fileLinePositionSpan.EndLinePosition.Character); + + var location = new RoslynIssueLocation( + diagnostic.GetMessage(), + fileLinePositionSpan.Path, + textRange); + + // todo SLVS-2427 quick fixes + // todo SLVS-2428 secondary locations + return new RoslynIssue( + SonarCompositeRuleId.GetFullErrorCode(language.RepoInfo.Key, diagnostic.Id), + location); + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs new file mode 100644 index 0000000000..9a127ecbb6 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs @@ -0,0 +1,36 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +public interface IRoslynCompilationWithAnalyzersWrapper +{ + Language Language { get; } + SyntaxTree? GetSyntaxTree(string filePath); + + SemanticModel? GetSemanticModel(string filePath); + + Task> GetAnalyzerSyntaxDiagnosticsAsync(SyntaxTree syntaxTree, CancellationToken token); + Task> GetAnalyzerSemanticDiagnosticsAsync(SemanticModel semanticModel, CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs new file mode 100644 index 0000000000..bb25ae9ad4 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs @@ -0,0 +1,36 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynCompilationWrapper +{ + CompilationOptions RoslynCompilationOptions { get; } + Language Language { get; } + + IRoslynCompilationWrapper WithOptions(CompilationOptions withSpecificDiagnosticOptions); + + IRoslynCompilationWithAnalyzersWrapper WithAnalyzers(ImmutableArray analyzers, CompilationWithAnalyzersOptions compilationWithAnalyzersOptions); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs new file mode 100644 index 0000000000..794889eef1 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs @@ -0,0 +1,39 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynProjectWrapper +{ + string Name { get; } + + bool SupportsCompilation { get; } + + AnalyzerOptions RoslynAnalyzerOptions { get; } + + bool ContainsDocument( + string filePath, + [NotNullWhen(true)]out string? analysisFilePath); + + Task GetCompilationAsync(CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs new file mode 100644 index 0000000000..e97c9d63f0 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynSolutionWrapper +{ + public IEnumerable Projects { get; } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs new file mode 100644 index 0000000000..e4e061676d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynWorkspaceWrapper +{ + IRoslynSolutionWrapper GetCurrentSolution(); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs new file mode 100644 index 0000000000..1f2579d792 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs @@ -0,0 +1,43 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +public class RoslynCompilationWithAnalyzersWrapper(CompilationWithAnalyzers compilation, Language language) : IRoslynCompilationWithAnalyzersWrapper +{ + public Language Language { get; } = language; + + public SyntaxTree? GetSyntaxTree(string filePath) => compilation.Compilation.SyntaxTrees.SingleOrDefault(x => filePath.Equals(x.FilePath)); + + public SemanticModel? GetSemanticModel(string filePath) => GetSyntaxTree(filePath) is {} syntaxTree ? compilation.Compilation.GetSemanticModel(syntaxTree) : null; + + public Task> GetAnalyzerSyntaxDiagnosticsAsync(SyntaxTree syntaxTree, CancellationToken token) => + compilation.GetAnalyzerSyntaxDiagnosticsAsync(syntaxTree, token); + + public Task> GetAnalyzerSemanticDiagnosticsAsync(SemanticModel semanticModel, CancellationToken token) => + compilation.GetAnalyzerSemanticDiagnosticsAsync(semanticModel, null, token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs new file mode 100644 index 0000000000..9e20e767d3 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs @@ -0,0 +1,45 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynCompilationWrapper(Compilation roslynCompilation) : IRoslynCompilationWrapper +{ + public CompilationOptions RoslynCompilationOptions => roslynCompilation.Options; + public Language Language { get; } = roslynCompilation.Language switch + { + LanguageNames.CSharp => Language.CSharp, + LanguageNames.VisualBasic => Language.VBNET, + _ => throw new ArgumentOutOfRangeException(nameof(roslynCompilation)), + }; + + public IRoslynCompilationWrapper WithOptions(CompilationOptions withSpecificDiagnosticOptions) => + new RoslynCompilationWrapper(roslynCompilation.WithOptions(withSpecificDiagnosticOptions)); + + public IRoslynCompilationWithAnalyzersWrapper WithAnalyzers(ImmutableArray analyzers, CompilationWithAnalyzersOptions compilationWithAnalyzersOptions) => + new RoslynCompilationWithAnalyzersWrapper(roslynCompilation.WithAnalyzers(analyzers, compilationWithAnalyzersOptions), Language); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs new file mode 100644 index 0000000000..4477de2036 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs @@ -0,0 +1,53 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynProjectWrapper(Project project) : IRoslynProjectWrapper +{ + public string Name => project.Name; + public bool SupportsCompilation => project.SupportsCompilation; + public AnalyzerOptions RoslynAnalyzerOptions => project.AnalyzerOptions; + + public async Task GetCompilationAsync(CancellationToken token) => + new RoslynCompilationWrapper((await project.GetCompilationAsync(token))!); + + public bool ContainsDocument( + string filePath, + [NotNullWhen(true)]out string? analysisFilePath) + { + analysisFilePath = project.Documents + .Select(document => document.FilePath) + .Where(path => path != null) + .FirstOrDefault(candidatePath => + candidatePath!.Equals(filePath) || IsAssociatedGeneratedFile(filePath, candidatePath)); + + return analysisFilePath != null; + } + + // cshtml razor files are converted into .\file.cshtml..g.cs files when included in the compilation + private static bool IsAssociatedGeneratedFile(string razorFilePath, string candidateDocumentPath) => + candidateDocumentPath.StartsWith(razorFilePath) && candidateDocumentPath.EndsWith(".g.cs"); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs new file mode 100644 index 0000000000..97b500ec7d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +internal class RoslynSolutionWrapper(Solution workspaceCurrentSolution) : IRoslynSolutionWrapper +{ + public IEnumerable Projects { get; } = workspaceCurrentSolution.Projects.Select(x => new RoslynProjectWrapper(x)); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs new file mode 100644 index 0000000000..ff68f60714 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs @@ -0,0 +1,35 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.LanguageServices; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +[Export(typeof(IRoslynWorkspaceWrapper))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynWorkspaceWrapper([Import(typeof(VisualStudioWorkspace))] Workspace workspace) : IRoslynWorkspaceWrapper +{ + public IRoslynSolutionWrapper GetCurrentSolution() => new RoslynSolutionWrapper(workspace.CurrentSolution); +} diff --git a/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj index 2e32f9e3b7..f9317efb0b 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj +++ b/src/RoslynAnalyzerServer/RoslynAnalyzerServer.csproj @@ -1,6 +1,8 @@  - + + + SonarLint.VisualStudio.RoslynAnalyzerServer diff --git a/src/RoslynAnalyzerServer/packages.lock.json b/src/RoslynAnalyzerServer/packages.lock.json index f23c2607b7..30432a6f46 100644 --- a/src/RoslynAnalyzerServer/packages.lock.json +++ b/src/RoslynAnalyzerServer/packages.lock.json @@ -30,6 +30,21 @@ "System.IO.Pipelines": "5.0.1" } }, + "Microsoft.VisualStudio.LanguageServices": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "igs65alqrom0ogrF/ehqc3g1bmekoZERDLHwIOcZDvruRFwhmTo3rBhOTiKEAUChg62aPWIvWB1mVYAgda1tMQ==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.EditorFeatures.Text": "[3.11.0]", + "Microsoft.CodeAnalysis.Features": "[3.11.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[3.11.0]", + "Microsoft.VisualStudio.Composition": "16.9.20", + "System.Threading.Tasks.Dataflow": "5.0.0" + } + }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", @@ -40,6 +55,30 @@ "resolved": "2.2.0", "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" }, + "MessagePack": { + "type": "Transitive", + "resolved": "2.1.152", + "contentHash": "PlJ31qf42uGuJfwc61x/Pt4hJi01xh1rrBofj1MJSLzEot/2UAIRdSgxEHN/8qou5CV8OBeDM9HXKPi1Oj8rpQ==", + "dependencies": { + "MessagePack.Annotations": "2.1.152", + "Microsoft.Bcl.AsyncInterfaces": "1.0.0", + "System.Memory": "4.5.3", + "System.Reflection.Emit": "4.6.0", + "System.Reflection.Emit.Lightweight": "4.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.1.152", + "contentHash": "RONktDA/HA641ds/2bfOqYSVew8o8EJMcQ1P4M1J77QGgbzWiWt3nBHvCAwlx0VfO6K9S8xq4b5OLD2CUnhtCg==" + }, + "MessagePackAnalyzer": { + "type": "Transitive", + "resolved": "2.1.152", + "contentHash": "uJhZlGMkXDaFYsH8V9S6o1EyvsUqB9mpU4DVBXNr0DXZVzZMhuLP1IkLj5xK3EKlaAcvkFkZv3eSvuz360wb3Q==" + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "5.0.0", @@ -53,6 +92,157 @@ "resolved": "3.3.2", "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" }, + "Microsoft.CodeAnalysis.AnalyzerUtilities": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "gyQ70pJ4T7hu/s0+QnEaXtYfeG/JrttGnxHJlrhpxsQjRIUGuRhVwNBtkHHYOrUAZ/l47L98/NiJX6QmTwAyrg==" + }, + "Microsoft.CodeAnalysis.EditorFeatures.Text": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "kzTA3dFcBB4M3ft/T4nSPi4fst4N7Vi37zUnWhhs0SAb/jEqeWvD+g4RkB4/0x2pg6YlaaK7ZXqosUqiizhGNw==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[3.11.0]", + "Microsoft.VisualStudio.CoreUtility": "16.10.230", + "Microsoft.VisualStudio.Text.Data": "16.10.230", + "Microsoft.VisualStudio.Text.Logic": "16.10.230", + "Microsoft.VisualStudio.Threading": "16.10.56" + } + }, + "Microsoft.CodeAnalysis.Features": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "FZi4btbqla9fxBbMBOrlwJIvOxmb80E32fQ69IhU4NNQiLSF3LIyOUSQvnV6xl1MccGET7W+NUPtEexsH8GNjA==", + "dependencies": { + "Microsoft.CodeAnalysis.AnalyzerUtilities": "3.3.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.11.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[3.11.0]", + "Microsoft.DiaSymReader": "1.3.0", + "Microsoft.VisualStudio.Debugger.Contracts": "16.10.0-beta.21214.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "3R0TwSj+74/iDl9R2MDwsGiyrYjCnIKs5a1e17Fa/jGI/C5/c3v/X7tAhnvdXl7pIUnXtTjyWrkVLTv5EKBPmQ==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.11.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "/fn1Tfo7j7k/slViPlM8azJuxQmri7FZ8dQ+gTeLbI29leN/1VK0U/BFcRdJNctsRCUgyKJ2q+I0Tjq07Rc1/Q==" + }, + "Microsoft.VisualStudio.Composition": { + "type": "Transitive", + "resolved": "16.9.20", + "contentHash": "7vrGl8+9p+vBpqfnQlQ0I2/2qRk3coYxszn9Mu12Xxxoqc/FfHcxJUyGgdqoc0o1EAwkXzKm7FNuyOHFhQ1McQ==", + "dependencies": { + "Microsoft.VisualStudio.Composition.Analyzers": "16.9.20", + "Microsoft.VisualStudio.Composition.NetFxAttributes": "16.9.20", + "Microsoft.VisualStudio.Validation": "15.5.31", + "System.ComponentModel.Composition": "4.5.0", + "System.Composition": "1.0.31", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Metadata": "1.6.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Threading.Tasks.Dataflow": "4.11.1" + } + }, + "Microsoft.VisualStudio.Composition.Analyzers": { + "type": "Transitive", + "resolved": "16.9.20", + "contentHash": "3SoUdxqmeczGmNwifgzlVDMtSYd3tyB/9qoqiN8dl9UVPI3uQKW2XwUQn86l34P3IabJMsj4EbHKZ8q9fkeDGw==" + }, + "Microsoft.VisualStudio.Composition.NetFxAttributes": { + "type": "Transitive", + "resolved": "16.9.20", + "contentHash": "HlPvzs95ONvqML2WsEyu9WpBSMYTwrKugeyyRXeH0x6qDVfYhkSVbZz9nLXJQstNMY+OY7aLu4xKZOwui9f6ag==", + "dependencies": { + "System.ComponentModel.Composition": "4.5.0" + } + }, + "Microsoft.VisualStudio.CoreUtility": { + "type": "Transitive", + "resolved": "16.10.230", + "contentHash": "segnoghs4hfsD3tpM8t+6gcP1PS4a8iHR2GfeAxQjuNQPNnbFSdB376O0wjAx5/qk8H4ZyZJ7fDfv0iP0ZTL/Q==", + "dependencies": { + "Microsoft.VisualStudio.Threading": "16.10.56", + "System.Collections.Immutable": "5.0.0", + "System.ComponentModel.Composition": "4.5.0" + } + }, + "Microsoft.VisualStudio.Debugger.Contracts": { + "type": "Transitive", + "resolved": "16.10.0-beta.21214.1", + "contentHash": "ybpSfj18yDue4O4YAljlkHrCFECjTAy/llhlDFE2NO5e586zdsjfqwJw5+ydVM2WCYQK+CwlB6YboKaqjnKGjA==", + "dependencies": { + "MessagePack": "2.1.152", + "MessagePackAnalyzer": "2.1.152", + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "System.Collections.Immutable": "1.5.0" + } + }, + "Microsoft.VisualStudio.Text.Data": { + "type": "Transitive", + "resolved": "16.10.230", + "contentHash": "6uKWOu5nlP5PyxFj8brV2n2k0OY9Ax1/JVgY6QEdhcDMHrCEhIyAiZO4SsDoMmTSv0rXfgxLTZpl/+nQjPyfEg==", + "dependencies": { + "Microsoft.VisualStudio.CoreUtility": "16.10.230", + "Microsoft.VisualStudio.Threading": "16.10.56" + } + }, + "Microsoft.VisualStudio.Text.Logic": { + "type": "Transitive", + "resolved": "16.10.230", + "contentHash": "+eQJ9gkbZfr/pFBJxjHNND9X4e2vjK7AByacbB2+vNen85dXhpwxvZnKYNlnbubtROwmc8CIPSaj+Kg3xHc9kA==", + "dependencies": { + "Microsoft.VisualStudio.CoreUtility": "16.10.230", + "Microsoft.VisualStudio.Text.Data": "16.10.230", + "System.Collections.Immutable": "5.0.0", + "System.ComponentModel.Composition": "4.5.0" + } + }, + "Microsoft.VisualStudio.Threading": { + "type": "Transitive", + "resolved": "16.10.56", + "contentHash": "RjV4Y+/Qh+nSFVPJJXby56W4Th5Z4fi8dBfJY5v8HsO7vA749OsqGdVVfUiXAySsZmJTh6cE9kM0faB/dgIPFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.VisualStudio.Threading.Analyzers": "16.10.56", + "Microsoft.VisualStudio.Validation": "16.9.32", + "Microsoft.Win32.Registry": "5.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Transitive", + "resolved": "16.10.56", + "contentHash": "uTvu9fbmHTtRFY5xUgG45waU+ARad0JxyG/2btOC9Su6RTo3PrGVXKU2vHDtVELqlJsnKImhMaPH3YR0XCVwDg==" + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "16.9.32", + "contentHash": "4Ozqy5OjtU4k9aKT76or3y8tVq6ju8+ba4YZHIe5wTb28jeCg11zkK6bdI4kj8s+k5OSekyO+FkUyoOkvPkgVg==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -71,6 +261,11 @@ "System.Memory": "4.5.4" } }, + "System.ComponentModel.Composition": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" + }, "System.Composition": { "type": "Transitive", "resolved": "1.0.31", @@ -149,6 +344,21 @@ "resolved": "4.5.0", "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==" + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "qAo4jyXtC9i71iElngX7P2r+zLaiHzxKwf66sc3X91tL5Ks6fnQ1vxL04o7ZSm3sYfLExySL7GN8aTpNYpU1qw==" + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" + }, "System.Reflection.Metadata": { "type": "Transitive", "resolved": "5.0.0", @@ -157,11 +367,32 @@ "System.Collections.Immutable": "5.0.0" } }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0" + } + }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, "System.Text.Encoding.CodePages": { "type": "Transitive", "resolved": "4.5.1", @@ -178,6 +409,11 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, + "System.Threading.Tasks.Dataflow": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NBp0zSAMZp4muDje6XmbDfmkqw9+qsDCHp+YMEtnVgHEjQZ3Q7MzFTTp3eHqpExn4BwMrS7JkUVOTcVchig4Sw==" + }, "System.Threading.Tasks.Extensions": { "type": "Transitive", "resolved": "4.5.4", From 29a853a9abe71a4633f4bbf36e70d10d1589114b Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:47:31 +0200 Subject: [PATCH 05/38] SLVS-2428 Provide secondary locations (#6370) [SLVS-2428](https://sonarsource.atlassian.net/browse/SLVS-2428) [SLVS-2428]: https://sonarsource.atlassian.net/browse/SLVS-2428?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../DiagnosticToRoslynIssueConverterTests.cs | 98 +++++++++++++++---- .../SonarRoslynDiagnosticsConverter.cs | 40 ++++++-- .../Resources.Designer.cs | 10 +- src/RoslynAnalyzerServer/Resources.resx | 3 + 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs index 77a0d19008..726ce43c5f 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using SonarLint.VisualStudio.Core; @@ -29,11 +30,14 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; [TestClass] public class DiagnosticToRoslynIssueConverterTests { + private static readonly ImmutableDictionary SecondaryLocationMessages = + new Dictionary { { "0", "First secondary message" }, { "1", "Second secondary message" } }.ToImmutableDictionary(); + private static readonly ImmutableDictionary? NoSecondaryLocationMessages = null; + private readonly DiagnosticToRoslynIssueConverter testSubject = new(); [TestMethod] - public void MefCtor_CheckExports() => - MefTestHelpers.CheckTypeCanBeImported(); + public void MefCtor_CheckExports() => MefTestHelpers.CheckTypeCanBeImported(); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); @@ -49,13 +53,20 @@ public void MefCtor_CheckExports() => [DataTestMethod] [DynamicData(nameof(TestData))] public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( - Language language, string ruleId, string message, string filePath, - int startLine, int endLine, int startChar, int endChar) + Language language, + string ruleId, + string message, + string filePath, + int startLine, + int endLine, + int startChar, + int endChar) { - var diagnostic = CreateDiagnostic(ruleId, message, filePath, startLine, endLine, startChar, endChar); + var location = CreateLocation(filePath, startLine, endLine, startChar, endChar); + var diagnostic = CreateDiagnostic(ruleId, message, location); var expectedTextRange = new RoslynIssueTextRange( - startLine + 1, // Convert to 1-based - endLine + 1, // Convert to 1-based + startLine + 1, // Convert to 1-based + endLine + 1, // Convert to 1-based startChar, endChar); var expectedLocation = new RoslynIssueLocation( @@ -72,22 +83,55 @@ public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( result.Should().BeEquivalentTo(expectedDiagnostic); } - private static Diagnostic CreateDiagnostic(string id, string message, string filePath, int startLine, int endLine, int startChar, int endChar) + public static object?[][] SecondaryLocationTestData => + [ + [ + SecondaryLocationMessages, + SecondaryLocationMessages.OrderBy(x => x.Key).Select(x => x.Value).ToArray() + ], + [ + NoSecondaryLocationMessages, + new[] { "Location 0", "Location 1" } + ] + ]; + + [DataTestMethod] + [DynamicData(nameof(SecondaryLocationTestData))] + public void ConvertToSonarDiagnostic_WithSecondaryLocations_ConvertsCorrectly( + ImmutableDictionary? properties, + string[] expectedMessages) { - var descriptor = new DiagnosticDescriptor( - id, - "Any Title", - message, - "Any Category", - default, - default); + const string fileCs = "c:\\test\\file.cs"; + const string file2Cs = "c:\\test\\file2.cs"; + var primaryLocation = CreateLocation(fileCs, 5, 5, 10, 15); + var additionalLocations = new[] { CreateLocation(fileCs, 10, 10, 20, 25), CreateLocation(file2Cs, 15, 15, 30, 35) }; + var diagnostic = CreateDiagnostic("any", "any", primaryLocation, additionalLocations, properties); + var expectedFlows = new[] + { + new RoslynIssueFlow(new List + { + new( + expectedMessages[0], + fileCs, + new RoslynIssueTextRange(11, 11, 20, 25)), + new( + expectedMessages[1], + file2Cs, + new RoslynIssueTextRange(16, 16, 30, 35)) + }) + }; - var location = CreateLocation(filePath, startLine, endLine, startChar, endChar); + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, Language.CSharp); - return Diagnostic.Create(descriptor, location); + result.Flows.Should().BeEquivalentTo(expectedFlows); } - private static Location CreateLocation(string filePath, int startLine, int endLine, int startChar, int endChar) + private static Location CreateLocation( + string filePath, + int startLine, + int endLine, + int startChar, + int endChar) { var textSpan = new TextSpan(12, 34); var syntaxTree = Substitute.For(); @@ -98,4 +142,22 @@ private static Location CreateLocation(string filePath, int startLine, int endLi return Location.Create(syntaxTree, textSpan); } + + private static Diagnostic CreateDiagnostic( + string id, + string message, + Location location, + Location[]? additionalLocations = null, + ImmutableDictionary? properties = null) + { + var descriptor = new DiagnosticDescriptor( + id, + "Any Title", + message, + "Any Category", + default, + default); + + return Diagnostic.Create(descriptor, location, additionalLocations: additionalLocations, properties: properties ?? ImmutableDictionary.Empty); + } } diff --git a/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs b/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs index bcfa9aeef9..312a039557 100644 --- a/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs +++ b/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs @@ -28,10 +28,38 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; [PartCreationPolicy(CreationPolicy.Shared)] public class DiagnosticToRoslynIssueConverter : IDiagnosticToRoslynIssueConverter { - public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language language) + public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language language) => + // todo SLVS-2427 quick fixes + new(SonarCompositeRuleId.GetFullErrorCode(language.RepoInfo.Key, diagnostic.Id), + ConvertLocation(diagnostic.Location.GetMappedLineSpan(), diagnostic.GetMessage()), + ConvertSecondaryLocations(diagnostic)); + + private static IReadOnlyList ConvertSecondaryLocations(Diagnostic diagnostic) { - var fileLinePositionSpan = diagnostic.Location.GetMappedLineSpan(); + if (diagnostic.AdditionalLocations.Count == 0) + { + return []; + } + + return + [ + // this will need to be modified once multi-flow locations are supported by the dotnet analyzer + new(diagnostic + .AdditionalLocations + .Select((location, index) => + { + if (!diagnostic.Properties.TryGetValue(index.ToString(), out var title) || title is null) + { + title = string.Format(Resources.DefaultSecondaryLocationTitleTemplate, index); + } + return ConvertLocation(location.GetMappedLineSpan(), title); + }) + .ToList()) + ]; + } + private static RoslynIssueLocation ConvertLocation(FileLinePositionSpan fileLinePositionSpan, string message) + { var textRange = new RoslynIssueTextRange( fileLinePositionSpan.StartLinePosition.Line + 1, // roslyn lines are 0-based, while we use 1-based fileLinePositionSpan.EndLinePosition.Line + 1, // roslyn lines are 0-based, while we use 1-based @@ -39,14 +67,10 @@ public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language lang fileLinePositionSpan.EndLinePosition.Character); var location = new RoslynIssueLocation( - diagnostic.GetMessage(), + message, fileLinePositionSpan.Path, textRange); - // todo SLVS-2427 quick fixes - // todo SLVS-2428 secondary locations - return new RoslynIssue( - SonarCompositeRuleId.GetFullErrorCode(language.RepoInfo.Key, diagnostic.Id), - location); + return location; } } diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs index 839784b696..85b72a1f12 100644 --- a/src/RoslynAnalyzerServer/Resources.Designer.cs +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -78,6 +77,15 @@ internal static string ConcurrentRequestsExceeded { } } + /// + /// Looks up a localized string similar to Location {0}. + /// + internal static string DefaultSecondaryLocationTitleTemplate { + get { + return ResourceManager.GetString("DefaultSecondaryLocationTitleTemplate", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error handling request: {0}.. /// diff --git a/src/RoslynAnalyzerServer/Resources.resx b/src/RoslynAnalyzerServer/Resources.resx index cac211d4d3..110a88a715 100644 --- a/src/RoslynAnalyzerServer/Resources.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -153,4 +153,7 @@ Staring http server for roslyn analysis... + + Location {0} + \ No newline at end of file From 569e52652348073acd4da18888209d97dd26c8de Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:02:07 +0200 Subject: [PATCH 06/38] SLVS-2413 add RoslynAnalysisConfigurationProvider (#6375) [SLVS-2413](https://sonarsource.atlassian.net/browse/SLVS-2413) Part of [SLVS-2413]: https://sonarsource.atlassian.net/browse/SLVS-2413?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Gabriela Trutan --- ...oslynAnalysisConfigurationProviderTests.cs | 197 ++++++++++++++++++ .../RoslynRuleConfigurationTests.cs | 74 +++++++ .../RoslynProjectCompilationProviderTests.cs | 6 +- .../Http/AnalysisRequestHandlerTest.cs | 4 +- .../IRoslynAnalysisConfigurationProvider.cs | 29 +++ .../IRoslynAnalysisProfilesProvider.cs | 36 ++++ .../Configuration/IRoslynAnalyzerProvider.cs | 32 +++ .../Configuration/ISonarLintXmlProvider.cs | 26 +++ .../RoslynAnalysisConfigurationProvider.cs | 75 +++++++ .../Configuration/RoslynRuleConfiguration.cs | 29 +++ .../SonarLintXmlConfigurationFile.cs | 41 ++++ .../Analysis/RoslynAnalysisConfiguration.cs | 5 +- .../RoslynProjectCompilationProvider.cs | 3 +- .../Http/Models/ActiveRuleDto.cs | 5 +- .../Resources.Designer.cs | 37 ++++ src/RoslynAnalyzerServer/Resources.resx | 12 ++ 16 files changed, 600 insertions(+), 11 deletions(-) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs new file mode 100644 index 0000000000..c6741ef79d --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -0,0 +1,197 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalysisConfigurationProviderTests +{ + private static readonly ImmutableDictionary DefaultAnalyzers + = new Dictionary { { Language.CSharp, new AnalyzersAndSupportedRules() } }.ToImmutableDictionary(); + private static readonly List DefaultActiveRules = new(); + private static readonly Dictionary DefaultAnalysisProperties = new(); + + private ISonarLintXmlProvider sonarLintXmlProvider = null!; + private IRoslynAnalyzerProvider roslynAnalyzerProvider = null!; + private IRoslynAnalysisProfilesProvider analyzerProfilesProvider = null!; + private TestLogger testLogger = null!; + private RoslynAnalysisConfigurationProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + sonarLintXmlProvider = Substitute.For(); + roslynAnalyzerProvider = Substitute.For(); + roslynAnalyzerProvider.GetAnalyzersByLanguage().Returns(DefaultAnalyzers); + + analyzerProfilesProvider = Substitute.For(); + testLogger = Substitute.ForPartsOf(); + + testSubject = new RoslynAnalysisConfigurationProvider( + sonarLintXmlProvider, + roslynAnalyzerProvider, + analyzerProfilesProvider, + testLogger); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_SetsLogContext() => + testLogger.Received(1).ForContext( + Resources.RoslynAnalysisLogContext, + Resources.RoslynAnalysisConfigurationLogContext); + + [TestMethod] + public void GetConfiguration_CreatesConfigurationForEachLanguage() + { + var roslynAnalysisProfiles = new Dictionary + { + { + Language.CSharp, new RoslynAnalysisProfile( + CreateTestAnalyzers(1), + [CreateRuleConfiguration(Language.CSharp, "S001"), CreateRuleConfiguration(Language.CSharp, "S002", false)], + new() { { "sonar.cs.property", "value" } }) + }, + { + Language.VBNET, new RoslynAnalysisProfile( + CreateTestAnalyzers(2), + [CreateRuleConfiguration(Language.VBNET, "S001", false), CreateRuleConfiguration(Language.VBNET, "S002")], + new() { { "sonar.vbnet.property", "value" } }) + } + }; + + var xmlConfigurations = SetUpXmlConfigurations(roslynAnalysisProfiles); + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(roslynAnalysisProfiles); + + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + + result.Keys.Should().BeEquivalentTo(roslynAnalysisProfiles.Keys); + foreach (var language in roslynAnalysisProfiles.Keys) + { + result.ContainsKey(language).Should().BeTrue(); + result[language].Analyzers.Should().BeEquivalentTo(roslynAnalysisProfiles[language].Analyzers); + result[language].DiagnosticOptions.Should().BeEquivalentTo(roslynAnalysisProfiles[language].Rules.ToDictionary(x => x.RuleId.RuleKey, x => x.ReportDiagnostic)); + result[language].SonarLintXml.Should().BeEquivalentTo(xmlConfigurations[language]); + } + } + + [TestMethod] + public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() + { + var language = Language.CSharp; + var roslynAnalysisProfiles = new Dictionary + { + { + language, new RoslynAnalysisProfile( + ImmutableArray.Empty, + [CreateRuleConfiguration(language, "S001")], + new Dictionary()) + } + }; + + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(roslynAnalysisProfiles); + + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + + result.Should().BeEmpty(); + testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoAnalyzers, language.Name)); + } + + [TestMethod] + public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() + { + var language = Language.CSharp; + var roslynAnalysisProfiles = new Dictionary + { + { + language, new RoslynAnalysisProfile( + CreateTestAnalyzers(1), + [CreateRuleConfiguration(language, "S001", false), CreateRuleConfiguration(language, "S002", false)], + new Dictionary()) + } + }; + + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(roslynAnalysisProfiles); + + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + + result.Should().BeEmpty(); + testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoActiveRules, language.Name)); + } + + [TestMethod] + public void GetConfiguration_NoAnalysisProfiles_ReturnsEmptyDictionary() + { + analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) + .Returns(new Dictionary()); + + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + + result.Should().BeEmpty(); + } + + private Dictionary SetUpXmlConfigurations(Dictionary profiles) + { + var xmlConfigurations = new Dictionary(); + foreach (var profile in profiles) + { + var xml = SetUpXmlProvider(profile.Value); + xmlConfigurations.Add(profile.Key, xml); + } + return xmlConfigurations; + } + + private RoslynRuleConfiguration CreateRuleConfiguration( + Language language, + string ruleKey, + bool isActive = true) => + new(new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey), + isActive, + []); + + private ImmutableArray CreateTestAnalyzers(int count) => Enumerable.Range(0, count).Select(_ => Substitute.For()).ToImmutableArray(); + + private SonarLintXmlConfigurationFile SetUpXmlProvider(RoslynAnalysisProfile profile) + { + var slxml = new SonarLintXmlConfigurationFile("any", "any"); + sonarLintXmlProvider.Create(profile).Returns(slxml); + return slxml; + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs new file mode 100644 index 0000000000..9892658384 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynRuleConfigurationTests.cs @@ -0,0 +1,74 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynRuleConfigurationTests +{ + [TestMethod] + public void Constructor_InitializesProperties() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var parameters = new Dictionary { { "param1", "value1" }, { "param2", "value2" } }; + var isActive = true; + + var testSubject = new RoslynRuleConfiguration(ruleId, isActive, parameters); + + testSubject.RuleId.Should().Be(ruleId); + testSubject.IsActive.Should().Be(isActive); + testSubject.Parameters.Should().BeSameAs(parameters); + } + + [TestMethod] + public void Constructor_NullParameters_AcceptsNull() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var isActive = true; + + var testSubject = new RoslynRuleConfiguration(ruleId, isActive, null); + + testSubject.RuleId.Should().Be(ruleId); + testSubject.IsActive.Should().Be(isActive); + testSubject.Parameters.Should().BeNull(); + } + + [TestMethod] + public void ReportDiagnostic_WhenActive_ReturnsWarn() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var testSubject = new RoslynRuleConfiguration(ruleId, true, null); + + testSubject.ReportDiagnostic.Should().Be(ReportDiagnostic.Warn); + } + + [TestMethod] + public void ReportDiagnostic_WhenInactive_ReturnsSuppress() + { + var ruleId = new SonarCompositeRuleId("cpp", "rule1"); + var testSubject = new RoslynRuleConfiguration(ruleId, false, null); + + testSubject.ReportDiagnostic.Should().Be(ReportDiagnostic.Suppress); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs index c7770e77ff..8f3811caa0 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs @@ -24,6 +24,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; using SonarLint.VisualStudio.TestInfrastructure; @@ -45,7 +46,7 @@ public class RoslynProjectCompilationProviderTests private AdditionalText existingAdditionalFile = null!; private TestLogger logger = null!; private IRoslynProjectWrapper project = null!; - private AdditionalText sonarLintXml = null!; + private SonarLintXmlConfigurationFile sonarLintXml = null!; private RoslynProjectCompilationProvider testSubject = null!; [TestInitialize] @@ -156,8 +157,7 @@ private void SetUpProject() private void SetUpAdditionalFiles() { - sonarLintXml = Substitute.For(); - sonarLintXml.Path.Returns(@"c:\path\to\SonarLint.xml"); + sonarLintXml = new SonarLintXmlConfigurationFile(@"C:\B\A", "content"); existingAdditionalFile = Substitute.For(); existingAdditionalFile.Path.Returns(@"c:\path\to\existing.txt"); diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs index a22e74d626..bda3ffcf85 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs @@ -269,7 +269,7 @@ public async Task ParseAnalysisRequestBody_FileNamesEmpty_ReturnsNull() [TestMethod] public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel() { - var validRequestJson = $"{{\"FileNames\":[\"{FileUri}\"],\"ActiveRules\":[{{\"RuleKey\":\"{DiagnosticId}\"}}]}}"; + var validRequestJson = $"{{\"FileNames\":[\"{FileUri}\"],\"ActiveRules\":[{{\"RuleId\":\"{DiagnosticId}\"}}]}}"; var stream = new MemoryStream(Encoding.UTF8.GetBytes(validRequestJson)); request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); @@ -280,7 +280,7 @@ public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel result!.FileNames.Should().HaveCount(1); result.FileNames[0].Should().Be(FileUri); result.ActiveRules.Should().HaveCount(1); - result.ActiveRules[0].RuleKey.Should().Be(DiagnosticId); + result.ActiveRules[0].RuleId.Should().Be(DiagnosticId); } private void MockValidRequest() diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs new file mode 100644 index 0000000000..bf4d616b74 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalysisConfigurationProvider +{ + IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs new file mode 100644 index 0000000000..fc67ea0af3 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs @@ -0,0 +1,36 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalysisProfilesProvider +{ + Dictionary GetAnalysisProfilesByLanguage( + ImmutableDictionary supportedRulesByLanguage, + List activeRules, + Dictionary? analysisProperties); +} + +internal record struct RoslynAnalysisProfile(ImmutableArray Analyzers, List Rules, Dictionary AnalysisProperties); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs new file mode 100644 index 0000000000..0c6d63793c --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs @@ -0,0 +1,32 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalyzerProvider +{ + ImmutableDictionary GetAnalyzersByLanguage(); +} + +internal record struct AnalyzersAndSupportedRules(ImmutableArray Analyzers, ImmutableArray SupportedRuleKeys); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs new file mode 100644 index 0000000000..ffa8498c24 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintXmlProvider.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface ISonarLintXmlProvider +{ + SonarLintXmlConfigurationFile Create(RoslynAnalysisProfile analysisProfile); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs new file mode 100644 index 0000000000..ec8889486b --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -0,0 +1,75 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalysisConfigurationProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynAnalysisConfigurationProvider( + ISonarLintXmlProvider sonarLintXmlProvider, + IRoslynAnalyzerProvider roslynAnalyzerProvider, + IRoslynAnalysisProfilesProvider analyzerProfilesProvider, + ILogger logger) : IRoslynAnalysisConfigurationProvider +{ + private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + + public IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties) + { + // todo add caching https://sonarsource.atlassian.net/browse/SLVS-2481 + + var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.GetAnalyzersByLanguage(), activeRules, analysisProperties); + + var configurations = new Dictionary(); + foreach (var analyzerAndLanguage in analysisProfilesByLanguage) + { + var language = analyzerAndLanguage.Key; + var analysisProfile = analyzerAndLanguage.Value; + + var languageLogContext = new MessageLevelContext { VerboseContext = [language.Id] }; + + if (analysisProfile is not { Analyzers.Length: > 0 }) + { + logger.LogVerbose(languageLogContext, Resources.RoslynAnalysisConfigurationNoAnalyzers, language.Name); + continue; + } + + if (!analysisProfile.Rules.Any(r => r.IsActive)) + { + logger.LogVerbose(languageLogContext, Resources.RoslynAnalysisConfigurationNoActiveRules, language.Name); + continue; + } + + configurations.Add( + language, + new RoslynAnalysisConfiguration( + sonarLintXmlProvider.Create(analysisProfile), + analysisProfile.Rules.ToImmutableDictionary(x => x.RuleId.RuleKey, y => y.ReportDiagnostic), + analysisProfile.Analyzers)); + } + + return configurations; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs new file mode 100644 index 0000000000..4fc62bbdc9 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynRuleConfiguration.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal readonly record struct RoslynRuleConfiguration(SonarCompositeRuleId RuleId, bool IsActive, Dictionary? Parameters) +{ + public ReportDiagnostic ReportDiagnostic => IsActive ? ReportDiagnostic.Warn : ReportDiagnostic.Suppress; +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs new file mode 100644 index 0000000000..82e5555283 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlConfigurationFile.cs @@ -0,0 +1,41 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal class SonarLintXmlConfigurationFile : AdditionalText +{ + private readonly SourceText sourceText; + + public override string Path { get; } + + public string FileName { get; } = "SonarLint.xml"; + + public SonarLintXmlConfigurationFile(string baseDirectory, string content) + { + Path = System.IO.Path.Combine(baseDirectory, FileName); + sourceText = SourceText.From(content); + } + + public override SourceText GetText(CancellationToken cancellationToken = default) => sourceText; +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs index 1e28060fe4..749604a5bb 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs @@ -21,10 +21,11 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; -public record RoslynAnalysisConfiguration( - AdditionalText SonarLintXml, +internal record RoslynAnalysisConfiguration( + SonarLintXmlConfigurationFile SonarLintXml, ImmutableDictionary DiagnosticOptions, ImmutableArray Analyzers); diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs index 2c0f5ef50d..3af45b885c 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs @@ -56,9 +56,8 @@ private IRoslynCompilationWithAnalyzersWrapper ApplyAnalyzersAndAdditionalFile( RoslynAnalysisConfiguration analysisConfigurationForLanguage) { var additionalFiles = project.RoslynAnalyzerOptions.AdditionalFiles; - var sonarLintXmlName = Path.GetFileName(analysisConfigurationForLanguage.SonarLintXml.Path); var analyzerOptions = project.RoslynAnalyzerOptions.WithAdditionalFiles(additionalFiles - .Where(x => Path.GetFileName(x.Path) != sonarLintXmlName) + .Where(x => Path.GetFileName(x.Path) != analysisConfigurationForLanguage.SonarLintXml.FileName) .Concat([analysisConfigurationForLanguage.SonarLintXml]) .ToImmutableArray()); diff --git a/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs b/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs index 6330b720ab..f8197f5d25 100644 --- a/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs +++ b/src/RoslynAnalyzerServer/Http/Models/ActiveRuleDto.cs @@ -20,5 +20,6 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; -// TODO by https://sonarsource.atlassian.net/browse/SLVS-2473 update DTO to match the one from plugin side -public record ActiveRuleDto(string RuleKey); +public record ActiveRuleDto( + string RuleId, + Dictionary Parameters); diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs index 85b72a1f12..ca46d49686 100644 --- a/src/RoslynAnalyzerServer/Resources.Designer.cs +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -175,5 +176,41 @@ internal static string HttpServerStarting { return ResourceManager.GetString("HttpServerStarting", resourceCulture); } } + + /// + /// Looks up a localized string similar to Configuration. + /// + internal static string RoslynAnalysisConfigurationLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisConfigurationLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No active rules loaded for language {0}. + /// + internal static string RoslynAnalysisConfigurationNoActiveRules { + get { + return ResourceManager.GetString("RoslynAnalysisConfigurationNoActiveRules", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No analyzers loaded for language {0}. + /// + internal static string RoslynAnalysisConfigurationNoAnalyzers { + get { + return ResourceManager.GetString("RoslynAnalysisConfigurationNoAnalyzers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Roslyn Analysis. + /// + internal static string RoslynAnalysisLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisLogContext", resourceCulture); + } + } } } diff --git a/src/RoslynAnalyzerServer/Resources.resx b/src/RoslynAnalyzerServer/Resources.resx index 110a88a715..49fec90dea 100644 --- a/src/RoslynAnalyzerServer/Resources.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -155,5 +155,17 @@ Location {0} + + + Roslyn Analysis + + + Configuration + + + No analyzers loaded for language {0} + + + No active rules loaded for language {0} \ No newline at end of file From c7ba736247b39aebc2eb018ee3a8026e877a7497 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:37:04 +0200 Subject: [PATCH 07/38] SLVS-2483 Add RoslynAnalysisProfilesProvider (#6377) [SLVS-2483](https://sonarsource.atlassian.net/browse/SLVS-2483) Part of SLVS-2406 [SLVS-2483]: https://sonarsource.atlassian.net/browse/SLVS-2483?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../RoslynAnalysisProfilesProviderTests.cs | 113 ++++++++++++++++++ .../RoslynAnalysisProfilesProvider.cs | 96 +++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs new file mode 100644 index 0000000000..159ca45890 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs @@ -0,0 +1,113 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalysisProfilesProviderTests +{ + private RoslynAnalysisProfilesProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() => testSubject = new RoslynAnalysisProfilesProvider(); + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void GetAnalysisProfilesByLanguage_EmptyInputs_ReturnsEmptyDictionary() + { + var supportedDiagnostics = ImmutableDictionary.Empty; + var activeRules = new List(); + Dictionary analysisProperties = []; + + var result = testSubject.GetAnalysisProfilesByLanguage(supportedDiagnostics, activeRules, analysisProperties); + + result.Should().BeEmpty(); + } + + [TestMethod] + public void GetAnalysisProfilesByLanguage_ReturnsFilteredRulesAndParameters() + { + var supportedDiagnostics = CreateSupportedDiagnosticsForLanguages(new() + { + { Language.CSharp, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"]) }, + { Language.VBNET, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"]) } + }); + List activeRules = + [ + new("csharpsquid:S001", new Dictionary { { "param1", "value1" } }), + new("csharpsquid:S003", []), + new("csharpsquid:SUNSUPPORTED", []), + new("vbnet:S002", new Dictionary { { "param2", "value2" } }) + ]; + var analysisProperties = new Dictionary { { "sonar.cs.property1", "value1" }, { "sonar.vbnet.property2", "value2" }, { "someotherkey", "value" } }; + + var result = testSubject.GetAnalysisProfilesByLanguage(supportedDiagnostics, activeRules, analysisProperties); + + result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); + ValidateProfile( + result[Language.CSharp], + supportedDiagnostics[Language.CSharp].Analyzers, + [ + CreateRuleConfiguration(Language.CSharp, "S001", new() { { "param1", "value1" } }), + CreateRuleConfiguration(Language.CSharp, "S002", isActive: false), + CreateRuleConfiguration(Language.CSharp, "S003", []) + ], + new() { { "sonar.cs.property1", "value1" } }); + ValidateProfile( + result[Language.VBNET], + supportedDiagnostics[Language.VBNET].Analyzers, + [ + CreateRuleConfiguration(Language.VBNET, "S001", isActive: false), + CreateRuleConfiguration(Language.VBNET, "S002", parameters: new() { { "param2", "value2" } }), + CreateRuleConfiguration(Language.VBNET, "S003", isActive: false) + ], + new Dictionary { { "sonar.vbnet.property2", "value2" } }); + } + + private static void ValidateProfile(RoslynAnalysisProfile profile, IEnumerable diagnosticAnalyzers, List rules, Dictionary analysisProperties) => + profile.Should().BeEquivalentTo(new RoslynAnalysisProfile(diagnosticAnalyzers.ToImmutableArray(), rules, analysisProperties), options => options.ComparingByMembers().ComparingByMembers()); + + private static RoslynRuleConfiguration CreateRuleConfiguration( + Language language, + string ruleKey, + Dictionary? parameters = null, + bool isActive = true) => + new(new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey), + isActive, + parameters); + + private static ImmutableDictionary CreateSupportedDiagnosticsForLanguages( + Dictionary analyzers) => + analyzers.ToImmutableDictionary( + x => x.Key, + y => new AnalyzersAndSupportedRules(y.Value.analyzers.ToImmutableArray(), y.Value.RuleKeys.ToImmutableArray())); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs new file mode 100644 index 0000000000..6479e99e36 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs @@ -0,0 +1,96 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalysisProfilesProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class RoslynAnalysisProfilesProvider : IRoslynAnalysisProfilesProvider +{ + public Dictionary GetAnalysisProfilesByLanguage( + ImmutableDictionary supportedRulesByLanguage, + List activeRules, + Dictionary? analysisProperties) + { + var roslynAnalysisProfiles = InitializeProfilesForEachLanguage(supportedRulesByLanguage); + AddRules(activeRules, supportedRulesByLanguage, roslynAnalysisProfiles); + AddProperties(analysisProperties, roslynAnalysisProfiles); + + return roslynAnalysisProfiles; + } + + private static Dictionary InitializeProfilesForEachLanguage(ImmutableDictionary supportedRulesByLanguage) + { + var roslynAnalysisProfiles = supportedRulesByLanguage.ToDictionary(x => x.Key, x => new RoslynAnalysisProfile(x.Value.Analyzers, [], [])); + return roslynAnalysisProfiles; + } + + private static void AddRules( + List activeRules, + ImmutableDictionary supportedRulesByLanguage, + Dictionary roslynAnalysisProfiles) + { + var activeRulesById = activeRules.ToDictionary(x => x.RuleId, y => y); + + foreach (var kvp in supportedRulesByLanguage) + { + var language = kvp.Key; + + if (!roslynAnalysisProfiles.TryGetValue(language, out var analysisProfile)) + { + continue; + } + + foreach (var ruleKey in kvp.Value.SupportedRuleKeys) + { + var ruleId = new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey); + analysisProfile.Rules.Add(activeRulesById.TryGetValue(ruleId.ErrorListErrorCode, out var activeRule) + ? new RoslynRuleConfiguration(ruleId, true, activeRule.Parameters) + : new RoslynRuleConfiguration(ruleId, false, null)); + } + } + } + + private static void AddProperties(Dictionary? analysisProperties, Dictionary roslynAnalysisProfiles) + { + if (analysisProperties == null) + { + return; + } + + foreach (var languageAndProfile in roslynAnalysisProfiles) + { + var prefix = $"sonar.{languageAndProfile.Key.ServerLanguageKey}."; + + foreach (var analysisProperty in analysisProperties.Where(analysisProperty => IsPropertyForLanguage(prefix, analysisProperty.Key))) + { + languageAndProfile.Value.AnalysisProperties.Add(analysisProperty.Key, analysisProperty.Value); + } + } + } + + private static bool IsPropertyForLanguage(string prefix, string propertyKey) => + propertyKey.StartsWith(prefix) && propertyKey.Length > prefix.Length; +} From 281d64f33d82ec2e012327e3282cff30ceb07ee4 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 20 Aug 2025 13:03:31 +0000 Subject: [PATCH 08/38] SLVS-2470 Provide server information to plugin --- src/CFamily.UnitTests/packages.lock.json | 1 + .../packages.lock.json | 1 + src/Integration.Vsix/packages.lock.json | 1 + .../HttpServerConfigurationProviderTest.cs | 15 +++++++++ .../Http/HttpServerConfigurationProvider.cs | 31 ++++++++++++++++--- .../Http/IHttpServerConfiguration.cs | 2 ++ .../FileAnalysisTestsRunner.cs | 12 ++++++- .../packages.lock.json | 1 + ...lysisConfigurationProviderListenerTests.cs | 22 ++++++++----- .../packages.lock.json | 7 +++++ .../AnalysisConfigurationProviderListener.cs | 6 ++-- src/SLCore.Listeners/SLCore.Listeners.csproj | 1 + src/SLCore.Listeners/packages.lock.json | 6 ++++ 13 files changed, 90 insertions(+), 16 deletions(-) diff --git a/src/CFamily.UnitTests/packages.lock.json b/src/CFamily.UnitTests/packages.lock.json index 38a4a6ff78..5860db2956 100644 --- a/src/CFamily.UnitTests/packages.lock.json +++ b/src/CFamily.UnitTests/packages.lock.json @@ -1407,6 +1407,7 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, diff --git a/src/Integration.Vsix.UnitTests/packages.lock.json b/src/Integration.Vsix.UnitTests/packages.lock.json index dc86230faa..ad6c4a81a3 100644 --- a/src/Integration.Vsix.UnitTests/packages.lock.json +++ b/src/Integration.Vsix.UnitTests/packages.lock.json @@ -1412,6 +1412,7 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, diff --git a/src/Integration.Vsix/packages.lock.json b/src/Integration.Vsix/packages.lock.json index f61acab1b3..9a607d6dd1 100644 --- a/src/Integration.Vsix/packages.lock.json +++ b/src/Integration.Vsix/packages.lock.json @@ -1562,6 +1562,7 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs index 9c70745c61..68511b82e4 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs @@ -101,6 +101,21 @@ public void SetNewConfiguration_GeneratesDifferentProperties() newConfig.Token.Should().NotBe(originalConfiguration.Token); } + [TestMethod] + public void MapToInferredProperties_ReturnsExpectedProperties() + { + var portKey = "sonar.cs.internal.roslynAnalyzerServerPort"; + var tokenKey = "sonar.cs.internal.roslynAnalyzerServerToken"; + + var analysisProperties = testSubject.CurrentConfiguration.MapToInferredProperties(); + + analysisProperties.Count.Should().Be(2); + analysisProperties.Should().ContainKey(portKey); + analysisProperties.Should().ContainKey(tokenKey); + analysisProperties[portKey].Should().Be(testSubject.CurrentConfiguration.Port.ToString()); + analysisProperties[tokenKey].Should().Be(testSubject.CurrentConfiguration.Token.ToUnsecureString()); + } + private static void VerifyValidPort(int port) { port.Should().BeGreaterThan(IPEndPoint.MinPort); diff --git a/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs b/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs index f1d3b656ad..862eaf281e 100644 --- a/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs @@ -39,24 +39,45 @@ internal interface IHttpServerConfigurationFactory [Export(typeof(IHttpServerConfigurationProvider))] [Export(typeof(IHttpServerConfigurationFactory))] [PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class HttpServerConfigurationProvider() : IHttpServerConfigurationProvider, IHttpServerConfigurationFactory +internal class HttpServerConfigurationProvider : IHttpServerConfigurationProvider, IHttpServerConfigurationFactory { - public IHttpServerConfiguration CurrentConfiguration { get; private set; } = new HttpServerConfiguration(); + private readonly object lockObj = new(); + private IHttpServerConfiguration currentConfiguration = null!; + + [ImportingConstructor] + public HttpServerConfigurationProvider() => SetNewConfiguration(); public IHttpServerConfiguration SetNewConfiguration() { - CurrentConfiguration = new HttpServerConfiguration(); - return CurrentConfiguration; + lock (lockObj) + { + currentConfiguration = new HttpServerConfiguration(); + return currentConfiguration; + } + } + + public IHttpServerConfiguration CurrentConfiguration + { + get + { + lock (lockObj) + { + return currentConfiguration; + } + } } private sealed class HttpServerConfiguration : IHttpServerConfiguration { private const int TokenByteLength = 32; + private const string PortAnalysisPropertyKey = "sonar.cs.internal.roslynAnalyzerServerPort"; + private const string TokenAnalysisPropertyKey = "sonar.cs.internal.roslynAnalyzerServerToken"; public int Port { get; } = GetAvailablePort(); public SecureString Token { get; } = GenerateSecureToken(); + public Dictionary MapToInferredProperties() => new() { { PortAnalysisPropertyKey, Port.ToString() }, { TokenAnalysisPropertyKey, Token.ToUnsecureString() } }; + private static int GetAvailablePort() { var listener = new TcpListener(IPAddress.Loopback, 0); diff --git a/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs b/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs index 4441ff8834..aa92a8031b 100644 --- a/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs +++ b/src/RoslynAnalyzerServer/Http/IHttpServerConfiguration.cs @@ -26,4 +26,6 @@ public interface IHttpServerConfiguration { int Port { get; } SecureString Token { get; } + + Dictionary MapToInferredProperties(); } diff --git a/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs b/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs index 5c7f20f691..8d6d42dcae 100644 --- a/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs +++ b/src/SLCore.IntegrationTests/FileAnalysisTestsRunner.cs @@ -24,6 +24,7 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Listener.Analysis; @@ -82,7 +83,7 @@ private FileAnalysisTestsRunner(string testClassName, Dictionary())); slCoreTestRunner.AddListener(analysisListener); slCoreTestRunner.AddListener(listFilesListener); - slCoreTestRunner.AddListener(new AnalysisConfigurationProviderListener(activeConfigScopeTracker, infrastructureLogger)); + slCoreTestRunner.AddListener(new AnalysisConfigurationProviderListener(activeConfigScopeTracker, MockHttpServerConfigurationProvider(), infrastructureLogger)); slCoreTestRunner.AddListener(getFileExclusionsListener); clientFileDtoFactory = new ClientFileDtoFactory(infrastructureLogger); @@ -280,6 +281,15 @@ public void Dispose() activeConfigScopeTracker?.Dispose(); slCoreTestRunner?.Dispose(); } + + private static IHttpServerConfigurationProvider MockHttpServerConfigurationProvider() + { + var provider = Substitute.For(); + var httpServerConfiguration = Substitute.For(); + httpServerConfiguration.MapToInferredProperties().Returns([]); + provider.CurrentConfiguration.Returns(httpServerConfiguration); + return provider; + } } public interface ITestingFile diff --git a/src/SLCore.IntegrationTests/packages.lock.json b/src/SLCore.IntegrationTests/packages.lock.json index 38a4a6ff78..5860db2956 100644 --- a/src/SLCore.IntegrationTests/packages.lock.json +++ b/src/SLCore.IntegrationTests/packages.lock.json @@ -1407,6 +1407,7 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, diff --git a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs index 46a22bb4db..07861be27d 100644 --- a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/AnalysisConfigurationProviderListenerTests.cs @@ -21,6 +21,7 @@ using NSubstitute.ReturnsExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Analysis; @@ -32,31 +33,33 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation.Analy public class AnalysisConfigurationProviderListenerTests { private IActiveConfigScopeTracker activeConfigScopeTracker; - private AnalysisConfigurationProviderListener testSubject; + private IHttpServerConfigurationProvider httpServerConfigurationProvider; private TestLogger logger; + private AnalysisConfigurationProviderListener testSubject; [TestInitialize] public void TestInitialize() { activeConfigScopeTracker = Substitute.For(); + httpServerConfigurationProvider = Substitute.For(); logger = Substitute.ForPartsOf(); testSubject = new AnalysisConfigurationProviderListener( - activeConfigScopeTracker, logger); + activeConfigScopeTracker, httpServerConfigurationProvider, logger); } [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void Ctor_InitializesLogContexts() => - logger.Received(1).ForContext(SLCoreStrings.SLCoreName, SLCoreStrings.SLCoreAnalysisConfigurationLogContext); + public void Ctor_InitializesLogContexts() => logger.Received(1).ForContext(SLCoreStrings.SLCoreName, SLCoreStrings.SLCoreAnalysisConfigurationLogContext); [TestMethod] public void GetBaseDirAsync_NoConfigurationScope_ReturnsNull() @@ -95,15 +98,18 @@ public void GetBaseDirAsync_ConfigurationScopeIdMatches_ReturnsCommandsBaseDir() [DataRow(null, [new string[0]])] [DataRow("", [new string[0]])] - [DataRow("configScopeId", [new[] {@"C:\file1"}])] - [DataRow("configScopeId123", [new[] {@"C:\file1", @"D:\file"}])] + [DataRow("configScopeId", [new[] { @"C:\file1" }])] + [DataRow("configScopeId123", [new[] { @"C:\file1", @"D:\file" }])] [DataTestMethod] - public void GetInferredAnalysisProperties_AnyValue_ReturnsEmptySet(string configScopeId, string[] files) + public void GetInferredAnalysisProperties_AnyValue_ReturnsHttpServerConfiguration(string configScopeId, string[] files) { + var expectedProperties = new Dictionary { { "prop1", "val1" } }; + httpServerConfigurationProvider.CurrentConfiguration.MapToInferredProperties().Returns(expectedProperties); + var result = testSubject.GetInferredAnalysisPropertiesAsync(new GetInferredAnalysisPropertiesParams(configScopeId, files.Select(x => new FileUri(x)).ToList())) .Result; - result.Should().BeEquivalentTo(new GetInferredAnalysisPropertiesResponse([]),config:options => options.ComparingByMembers()); + result.Should().BeEquivalentTo(new GetInferredAnalysisPropertiesResponse(expectedProperties), options => options.ComparingByMembers()); } } diff --git a/src/SLCore.Listeners.UnitTests/packages.lock.json b/src/SLCore.Listeners.UnitTests/packages.lock.json index cb60c76013..a8bc9231ee 100644 --- a/src/SLCore.Listeners.UnitTests/packages.lock.json +++ b/src/SLCore.Listeners.UnitTests/packages.lock.json @@ -1321,6 +1321,12 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + }, "SonarLint.VisualStudio.SLCore": { "type": "Project", "dependencies": { @@ -1333,6 +1339,7 @@ "dependencies": { "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", + "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )" } }, diff --git a/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs b/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs index 40e125e076..8238d03fb0 100644 --- a/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs +++ b/src/SLCore.Listeners/Implementation/Analysis/AnalysisConfigurationProviderListener.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Analysis; @@ -29,7 +30,8 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation.Analysis; [Export(typeof(ISLCoreListener))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class AnalysisConfigurationProviderListener(IActiveConfigScopeTracker activeConfigScopeTracker, ILogger logger) : IAnalysisConfigurationProviderListener +internal class AnalysisConfigurationProviderListener(IActiveConfigScopeTracker activeConfigScopeTracker, IHttpServerConfigurationProvider httpServerConfigurationProvider, ILogger logger) + : IAnalysisConfigurationProviderListener { private readonly ILogger analysisConfigLogger = logger.ForContext(SLCoreStrings.SLCoreName, SLCoreStrings.SLCoreAnalysisConfigurationLogContext); @@ -52,5 +54,5 @@ public Task GetBaseDirAsync(GetBaseDirParams parameters) } public Task GetInferredAnalysisPropertiesAsync(GetInferredAnalysisPropertiesParams parameters) => - Task.FromResult(new GetInferredAnalysisPropertiesResponse([])); + Task.FromResult(new GetInferredAnalysisPropertiesResponse(httpServerConfigurationProvider.CurrentConfiguration.MapToInferredProperties())); } diff --git a/src/SLCore.Listeners/SLCore.Listeners.csproj b/src/SLCore.Listeners/SLCore.Listeners.csproj index adcdc92ee2..ff9e540b34 100644 --- a/src/SLCore.Listeners/SLCore.Listeners.csproj +++ b/src/SLCore.Listeners/SLCore.Listeners.csproj @@ -10,6 +10,7 @@ + diff --git a/src/SLCore.Listeners/packages.lock.json b/src/SLCore.Listeners/packages.lock.json index 8d168e41ae..4cfe80fb1f 100644 --- a/src/SLCore.Listeners/packages.lock.json +++ b/src/SLCore.Listeners/packages.lock.json @@ -1197,6 +1197,12 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, + "SonarLint.VisualStudio.RoslynAnalyzerServer": { + "type": "Project", + "dependencies": { + "SonarLint.VisualStudio.Core": "[1.0.0, )" + } + }, "SonarLint.VisualStudio.SLCore": { "type": "Project", "dependencies": { From 46d8e9ca587a989d9643341659a608bef4be08a5 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 22 Aug 2025 07:51:56 +0000 Subject: [PATCH 09/38] SLVS-2416 Embed SqvsRoslyn plugin in vsix extension (#6384) --- build/CopyDependencies/CopyDependencies.targets | 2 ++ build/DownloadDependencies/CommonProperties.props | 7 +++++++ build/DownloadDependencies/JarProcessing.targets | 8 ++++++++ src/EmbeddedSonarAnalyzer.props | 1 + 4 files changed, 18 insertions(+) diff --git a/build/CopyDependencies/CopyDependencies.targets b/build/CopyDependencies/CopyDependencies.targets index ed49120aad..f737b5d69c 100644 --- a/build/CopyDependencies/CopyDependencies.targets +++ b/build/CopyDependencies/CopyDependencies.targets @@ -37,6 +37,8 @@ + + diff --git a/build/DownloadDependencies/CommonProperties.props b/build/DownloadDependencies/CommonProperties.props index e112a6389f..ee7514765e 100644 --- a/build/DownloadDependencies/CommonProperties.props +++ b/build/DownloadDependencies/CommonProperties.props @@ -58,6 +58,13 @@ $(JarDownloadDir)\$(SonarTextPluginFileName) https://repox.jfrog.io/artifactory/sonarsource/org/sonarsource/text/sonar-text-plugin/$(EmbeddedSonarSecretsJarVersion)/$(SonarTextPluginFileName) + + + sonarqube-ide-visualstudio-roslyn-plugin-$(EmbeddedSonarSqvsRoslynJarVersion).jar + $(JarDownloadDir)\$(SonarSqvsRoslynPluginFileName) + + https://repox.jfrog.io/pathToPlugin/$(EmbeddedSonarSqvsRoslynJarVersion)/$(SonarSqvsRoslynPluginFileName) + $(LOCALAPPDATA)\SLVS_Build_Dotnet diff --git a/build/DownloadDependencies/JarProcessing.targets b/build/DownloadDependencies/JarProcessing.targets index e75dbc19f8..6f44af6d09 100644 --- a/build/DownloadDependencies/JarProcessing.targets +++ b/build/DownloadDependencies/JarProcessing.targets @@ -54,6 +54,14 @@ + + + + + + + diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 23624d9d81..0f8193d30e 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,6 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 + 1.0.0 10.34.0.83431 1.0.0 From 028f28efb3b92a7d2967674c9455cc8626ca6cae Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:08:30 +0200 Subject: [PATCH 10/38] SLVS-2414 Implement `RequestAnalysis` method (#6380) [SLVS-2414](https://sonarsource.atlassian.net/browse/SLVS-2414) Part of [SLVS-2414]: https://sonarsource.atlassian.net/browse/SLVS-2414?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../Http/Helper/HttpServerStarter.cs | 11 +-- .../Http/RoslynAnalysisHttpServerTest.cs | 10 +-- .../SequentialRoslynAnalysisEngineTests.cs | 4 +- .../Http/RoslynAnalysisHttpServerTest.cs | 8 +- .../RoslynAnalysisServiceTests.cs | 77 +++++++++++++++++++ .../Analysis/DiagnosticDuplicatesComparer.cs | 4 +- .../Analysis/IRoslynAnalysisEngine.cs | 3 +- .../IRoslynProjectCompilationProvider.cs | 2 +- .../Analysis/RoslynAnalysisConfiguration.cs | 2 +- .../Analysis/RoslynIssue.cs | 4 +- .../RoslynProjectCompilationProvider.cs | 2 +- .../SequentialRoslynAnalysisEngine.cs | 4 +- .../Http/AnalysisRequestHandler.cs | 7 +- .../Http/Models/AnalysisRequest.cs | 1 + .../Http/Models/AnalysisResponse.cs | 4 +- .../Http/RoslynAnalysisHttpServer.cs | 6 +- ...nosticDto.cs => IRoslynAnalysisService.cs} | 11 ++- ...ysisEngine.cs => RoslynAnalysisService.cs} | 22 +++--- 18 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs rename src/RoslynAnalyzerServer/{Http/Models/DiagnosticDto.cs => IRoslynAnalysisService.cs} (65%) rename src/RoslynAnalyzerServer/{AnalysisEngine.cs => RoslynAnalysisService.cs} (55%) diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs index 77cb709791..2c3b55214f 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs @@ -19,6 +19,7 @@ */ using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.SLCore.Common.Models; @@ -32,7 +33,7 @@ internal sealed class HttpServerStarter : IDisposable internal IHttpServerConfigurationProvider HttpServerConfigurationProvider { get; } internal RoslynAnalysisHttpServer RoslynAnalysisHttpServer { get; } internal ILogger MockedLogger { get; } = CreateMockedLogger(); - internal IAnalysisEngine MockedAnalysisEngine { get; } = CreateMockedAnalysisEngine(); + internal IRoslynAnalysisService MockedRoslynAnalysisService { get; } = CreateMockedAnalysisEngine(); public HttpServerStarter(bool useMockedServerSettings = false, int maxConcurrentRequests = 5, bool useMockedServerConfiguration = false) { @@ -42,7 +43,7 @@ public HttpServerStarter(bool useMockedServerSettings = false, int maxConcurrent var httpServerConfigurationFactory = useMockedServerConfiguration ? CreateHttpServerConfigurationFactory() : httpServerConfigurationProvider; var analysisRequestHandler = new AnalysisRequestHandler(MockedLogger, ServerSettings, HttpServerConfigurationProvider); RoslynAnalysisHttpServer = new RoslynAnalysisHttpServer(MockedLogger, ServerSettings, analysisRequestHandler, new HttpRequestHandler(), - new HttpListenerFactory(), httpServerConfigurationFactory, MockedAnalysisEngine); + new HttpListenerFactory(), httpServerConfigurationFactory, MockedRoslynAnalysisService); } public void StartListeningOnBackgroundThread() @@ -72,10 +73,10 @@ private static ILogger CreateMockedLogger() return logger; } - private static IAnalysisEngine CreateMockedAnalysisEngine() + private static IRoslynAnalysisService CreateMockedAnalysisEngine() { - var analysisEngine = Substitute.For(); - analysisEngine.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()).Returns(Task.FromResult(new List())); + var analysisEngine = Substitute.For(); + analysisEngine.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), Arg.Any()).Returns(Task.FromResult(Enumerable.Empty())); return analysisEngine; } diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs index f8982cd5e4..780d2107d2 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs @@ -109,7 +109,7 @@ public async Task StartListenAsync_AnalysisRequestTakesLongerThanTimeout_ClosesR var millisecondTimeout = 5; using var serverStarter2 = new HttpServerStarter(useMockedServerSettings: true); MockServerSettings(serverStarter2.ServerSettings, requestTimeout: millisecondTimeout); - SimulateLongAnalysis(serverStarter2.MockedAnalysisEngine, millisecondTimeout * 2); + SimulateLongAnalysis(serverStarter2.MockedRoslynAnalysisService, millisecondTimeout * 2); serverStarter2.StartListeningOnBackgroundThread(); var response = await HttpRequester.SendRequest(CreateClientRequestConfig(serverStarter2)); @@ -242,7 +242,7 @@ private static async Task VerifyRequestSucceeded(HttpResponseMessage response) response.StatusCode.Should().Be(HttpStatusCode.OK); var analysisResponse = await GetAnalysisResponse(response); analysisResponse.Should().NotBeNull(); - analysisResponse!.Diagnostics.Should().BeEmpty(); + analysisResponse!.RoslynIssues.Should().BeEmpty(); } private static async Task VerifyServerNotReachable(AnalysisRequestConfig analysisRequestConfig) where T : Exception @@ -270,8 +270,8 @@ private static void MockServerSettings( private static void MockServerConfiguration(IHttpServerConfigurationProvider serverConfigurationProvider, int port) => serverConfigurationProvider.CurrentConfiguration.Port.Returns(port); - private static void SimulateLongAnalysis(IAnalysisEngine analysisEngine, int milliseconds) => - analysisEngine - .When(x => x.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any())) + private static void SimulateLongAnalysis(IRoslynAnalysisService roslynAnalysisService, int milliseconds) => + roslynAnalysisService + .When(x => x.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), Arg.Any())) .Do(_ => Task.Delay(milliseconds).GetAwaiter().GetResult()); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs index 70407dfea9..d5d8246f86 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs @@ -114,7 +114,7 @@ public async Task AnalyzeAsync_DuplicateDiagnostics_ReturnsSingleDiagnostic() result.Should().BeEquivalentTo(duplicateIssue1); VerifyAnalysisExecution(requestForProject, compilationForProject, [duplicateDiagnostic1, duplicateDiagnostic2]); logger.AssertPartialOutputStringExists( - $"Duplicate diagnostic discarded ID: {duplicateIssue2.RuleKey}, File: {duplicateIssue2.PrimaryLocation.FilePath}, Line: {duplicateIssue2.PrimaryLocation.TextRange.StartLine}"); + $"Duplicate diagnostic discarded ID: {duplicateIssue2.RuleId}, File: {duplicateIssue2.PrimaryLocation.FilePath}, Line: {duplicateIssue2.PrimaryLocation.TextRange.StartLine}"); } [TestMethod] @@ -131,7 +131,7 @@ public async Task AnalyzeAsync_DuplicateDiagnosticsInDifferentProjects_ReturnsSi VerifyAnalysisExecution(requestForProject1, compilationForProject1, [diagnostic1]); VerifyAnalysisExecution(requestForProject2, compilationForProject2, [diagnostic2]); logger.AssertPartialOutputStringExists( - $"Duplicate diagnostic discarded ID: {duplicateIssue.RuleKey}, File: {duplicateIssue.PrimaryLocation.FilePath}, Line: {duplicateIssue.PrimaryLocation.TextRange.StartLine}"); + $"Duplicate diagnostic discarded ID: {duplicateIssue.RuleId}, File: {duplicateIssue.PrimaryLocation.FilePath}, Line: {duplicateIssue.PrimaryLocation.TextRange.StartLine}"); } [TestMethod] diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs index e5c7d24f14..7e5df39089 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs @@ -32,7 +32,7 @@ public class RoslynAnalysisHttpServerTest private static ILogger _logger = null!; private static IHttpServerSettings _configuration = null!; private static IAnalysisRequestHandler _analysisRequestHandler = null!; - private static IAnalysisEngine _analysisEngine = null!; + private static IRoslynAnalysisService _roslynAnalysisService = null!; private static RoslynAnalysisHttpServer _testSubject = null!; [ClassInitialize] @@ -43,10 +43,10 @@ public static void TestInitialize(TestContext context) _configuration = Substitute.For(); _analysisRequestHandler = Substitute.For(); _httpRequestHandler = Substitute.For(); - _analysisEngine = Substitute.For(); + _roslynAnalysisService = Substitute.For(); _serverConfigurationFactory = Substitute.For(); _testSubject = new RoslynAnalysisHttpServer(_logger, _configuration, _analysisRequestHandler, _httpRequestHandler, new HttpListenerFactory(), - _serverConfigurationFactory, _analysisEngine); + _serverConfigurationFactory, _roslynAnalysisService); } [ClassCleanup] @@ -61,7 +61,7 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs new file mode 100644 index 0000000000..bde7321b13 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -0,0 +1,77 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; +using SonarLint.VisualStudio.TestInfrastructure; +using Language = SonarLint.VisualStudio.Core.Language; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests; + +[TestClass] +public class RoslynAnalysisServiceTests +{ + private static readonly List DefaultActiveRules = new() { new("sample-rule-id", new() { { "paramKey", "paramValue" } }) }; + private static readonly Dictionary DefaultAnalysisProperties = new() { { "sonar.cs.any", "any" }, }; + private static readonly Dictionary DefaultAnalysisConfigurations = new() { { Language.CSharp, new RoslynAnalysisConfiguration() } }; + private static readonly List DefaultProjectAnalysisRequests = new() { new(Substitute.For(), []) }; + private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new("any", "any", new(1, 1, 1, 1))) }; + + private IRoslynAnalysisEngine analysisEngine = null!; + private IRoslynAnalysisConfigurationProvider analysisConfigurationProvider = null!; + private IRoslynSolutionAnalysisCommandProvider analysisCommandProvider = null!; + private RoslynAnalysisService testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + analysisEngine = Substitute.For(); + analysisConfigurationProvider = Substitute.For(); + analysisCommandProvider = Substitute.For(); + + testSubject = new RoslynAnalysisService(analysisEngine, analysisConfigurationProvider, analysisCommandProvider); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task AnalyzeAsync_PassesCorrectArgumentsToEngine() + { + string[] filePaths = [@"C:\file1.cs", @"C:\folder\file2.cs"]; + analysisConfigurationProvider.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties).Returns(DefaultAnalysisConfigurations); + analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(Arg.Is(x => x.SequenceEqual(filePaths))).Returns(DefaultProjectAnalysisRequests); + analysisEngine.AnalyzeAsync(DefaultProjectAnalysisRequests, DefaultAnalysisConfigurations, Arg.Any()).Returns(DefaultIssues); + + var issues = await testSubject.AnalyzeAsync(filePaths.Select(x => new FileUri(x)).ToList(), DefaultActiveRules, DefaultAnalysisProperties, CancellationToken.None); + + issues.Should().BeSameAs(DefaultIssues); + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs index 9482263d9f..9bd47c3178 100644 --- a/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs @@ -43,14 +43,14 @@ public bool Equals(RoslynIssue? x, RoslynIssue? y) return false; } - return x.RuleKey == y.RuleKey && LocationEquals(x.PrimaryLocation, y.PrimaryLocation); + return x.RuleId == y.RuleId && LocationEquals(x.PrimaryLocation, y.PrimaryLocation); } public int GetHashCode(RoslynIssue obj) { unchecked { - var hc = obj.RuleKey.GetHashCode(); + var hc = obj.RuleId.GetHashCode(); const int prime = 397; hc = (hc * prime) ^ obj.PrimaryLocation.FilePath.GetHashCode(); hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLine; diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs index ad66174aa9..24c7e6cb17 100644 --- a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Immutable; using SonarLint.VisualStudio.Core; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; @@ -27,6 +26,6 @@ internal interface IRoslynAnalysisEngine { Task> AnalyzeAsync( List projectsAnalysis, - ImmutableDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token); } diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs index 843356d871..fd7767488e 100644 --- a/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs @@ -28,6 +28,6 @@ internal interface IRoslynProjectCompilationProvider { Task GetProjectCompilationAsync( IRoslynProjectWrapper project, - ImmutableDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token); } diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs index 749604a5bb..6b2c1d6799 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs @@ -25,7 +25,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; -internal record RoslynAnalysisConfiguration( +internal record struct RoslynAnalysisConfiguration( SonarLintXmlConfigurationFile SonarLintXml, ImmutableDictionary DiagnosticOptions, ImmutableArray Analyzers); diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs index 77696e3bc1..8c22b2bd26 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs @@ -21,13 +21,13 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; public class RoslynIssue( - string ruleKey, + string ruleId, RoslynIssueLocation primaryLocation, IReadOnlyList? flows = null) { private static readonly IReadOnlyList EmptyFlows = []; - public string RuleKey { get; } = ruleKey; + public string RuleId { get; } = ruleId; public RoslynIssueLocation PrimaryLocation { get; } = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); public IReadOnlyList Flows { get; } = flows ?? EmptyFlows; } diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs index 3af45b885c..9cd199195c 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs @@ -37,7 +37,7 @@ internal class RoslynProjectCompilationProvider(ILogger logger) : IRoslynProject public async Task GetProjectCompilationAsync( IRoslynProjectWrapper project, - ImmutableDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token) { var compilation = await project.GetCompilationAsync(token); diff --git a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs index a1ac25ccf6..85b4301e08 100644 --- a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs @@ -37,7 +37,7 @@ internal class SequentialRoslynAnalysisEngine( public async Task> AnalyzeAsync( List projectsAnalysis, - ImmutableDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token) { var uniqueDiagnostics = new HashSet(DiagnosticDuplicatesComparer.Instance); @@ -55,7 +55,7 @@ public async Task> AnalyzeAsync( // todo SLVS-2468 improve issue merging if (!uniqueDiagnostics.Add(diagnostic)) { - logger.LogVerbose("Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}", diagnostic.RuleKey, Path.GetFileName(diagnostic.PrimaryLocation.FilePath), diagnostic.PrimaryLocation.TextRange.StartLine); + logger.LogVerbose("Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}", diagnostic.RuleId, Path.GetFileName(diagnostic.PrimaryLocation.FilePath), diagnostic.PrimaryLocation.TextRange.StartLine); } } } diff --git a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs index cfa35edb36..88076a4b1d 100644 --- a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs +++ b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs @@ -24,6 +24,7 @@ using System.Net.Http; using Newtonsoft.Json; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; @@ -33,7 +34,7 @@ public interface IAnalysisRequestHandler { Task ParseAnalysisRequestBody(IHttpListenerContext context); - string ParseAnalysisRequestResponse(List diagnostics); + string ParseAnalysisRequestResponse(List diagnostics); HttpStatusCode ValidateRequest(IHttpListenerContext context); } @@ -80,9 +81,9 @@ public HttpStatusCode ValidateRequest(IHttpListenerContext context) return null; } - public string ParseAnalysisRequestResponse(List diagnostics) + public string ParseAnalysisRequestResponse(List diagnostics) { - var responseObj = new AnalysisResponse { Diagnostics = diagnostics }; + var responseObj = new AnalysisResponse { RoslynIssues = diagnostics }; var responseString = JsonConvert.SerializeObject(responseObj); return responseString; } diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs index 2d43e120fc..3454a47155 100644 --- a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs @@ -26,4 +26,5 @@ public record AnalysisRequest { public List FileNames { get; set; } = []; public List ActiveRules { get; set; } = []; + public Dictionary AnalysisProperties { get; set; } = []; } diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs index 23c2e95c61..2a50cf695a 100644 --- a/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisResponse.cs @@ -18,9 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; public record AnalysisResponse { - public List Diagnostics { get; set; } = []; + public List RoslynIssues { get; set; } = []; } diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs index c7e7b41403..a722cfd25c 100644 --- a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -35,7 +35,7 @@ internal sealed class RoslynAnalysisHttpServer( IHttpRequestHandler httpRequestHandler, IHttpListenerFactory httpListenerFactory, IHttpServerConfigurationFactory httpServerConfigurationFactory, - IAnalysisEngine analysisEngine) : IRoslynAnalysisHttpServer + IRoslynAnalysisService roslynAnalysisService) : IRoslynAnalysisHttpServer { private readonly CancellationTokenSource cancellationTokenSource = new(); private readonly ILogger logger = logger.ForContext(Resources.HttpServerLogContext).ForContext(nameof(RoslynAnalysisHttpServer)); @@ -153,8 +153,8 @@ private async Task HandleRequest(IHttpListenerContext context, CancellationToken return; } - var diagnostics = await analysisEngine.AnalyzeAsync(analysisRequest.FileNames, analysisRequest.ActiveRules, cancellationToken); + var issues = await roslynAnalysisService.AnalyzeAsync(analysisRequest.FileNames, analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - await httpRequestHandler.SendResponse(context, analysisRequestHandler.ParseAnalysisRequestResponse(diagnostics)); + await httpRequestHandler.SendResponse(context, analysisRequestHandler.ParseAnalysisRequestResponse(issues.ToList())); } } diff --git a/src/RoslynAnalyzerServer/Http/Models/DiagnosticDto.cs b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs similarity index 65% rename from src/RoslynAnalyzerServer/Http/Models/DiagnosticDto.cs rename to src/RoslynAnalyzerServer/IRoslynAnalysisService.cs index 4e5f081c52..4e2a33aa1b 100644 --- a/src/RoslynAnalyzerServer/Http/Models/DiagnosticDto.cs +++ b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs @@ -18,7 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; -// TODO by https://sonarsource.atlassian.net/browse/SLVS-2473 update DTO to match the one from plugin side -public record DiagnosticDto(string Id); +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; +internal interface IRoslynAnalysisService +{ + Task> AnalyzeAsync(List files, List activeRules, Dictionary analysisProperties, CancellationToken cancellationToken); +} diff --git a/src/RoslynAnalyzerServer/AnalysisEngine.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs similarity index 55% rename from src/RoslynAnalyzerServer/AnalysisEngine.cs rename to src/RoslynAnalyzerServer/RoslynAnalysisService.cs index 1a3327892a..426ca33e5d 100644 --- a/src/RoslynAnalyzerServer/AnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -19,21 +19,25 @@ */ using System.ComponentModel.Composition; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer; -// TODO by https://sonarsource.atlassian.net/browse/SLVS-2473 replace with real analysis engine -internal interface IAnalysisEngine -{ - Task> AnalyzeAsync(List fileNames, List activeRules, CancellationToken cancellationToken); -} - -[Export(typeof(IAnalysisEngine))] +[Export(typeof(IRoslynAnalysisService))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class AnalysisEngine() : IAnalysisEngine +internal class RoslynAnalysisService(IRoslynAnalysisEngine analysisEngine, IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService { - public Task> AnalyzeAsync(List fileNames, List activeRules, CancellationToken cancellationToken) => Task.FromResult(new List()); + public Task> AnalyzeAsync( + List files, + List activeRules, + Dictionary analysisProperties, + CancellationToken cancellationToken) => + analysisEngine.AnalyzeAsync( + analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(files.Select(x => x.LocalPath).ToArray()), + analysisConfigurationProvider.GetConfiguration(activeRules, analysisProperties), + cancellationToken); } From 12516eeb182536a04b5c6226616dad63499532dd Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:13:38 +0200 Subject: [PATCH 11/38] SLVS-2485 Add SonarLintXmlProvider (#6378) [SLVS-2485](https://sonarsource.atlassian.net/browse/SLVS-2485) Part of SLVS-2406 [SLVS-2485]: https://sonarsource.atlassian.net/browse/SLVS-2485?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- src/Core/CSharpVB/SonarLintConfiguration.cs | 4 +- .../CSharpVB/RoslynConfigGeneratorTests.cs | 86 ----------- .../CSharpVB/RoslynConfigGenerator.cs | 7 +- ...onarLintConfigurationXmlSerializerTests.cs | 40 ++--- .../SonarLintXmlProviderTests.cs | 137 ++++++++++++++++++ .../ISonarLintConfigurationXmlSerializer.cs | 28 ++++ .../SonarLintConfigurationXmlSerializer.cs} | 7 +- .../Configuration/SonarLintXmlProvider.cs | 50 +++++++ 8 files changed, 242 insertions(+), 117 deletions(-) delete mode 100644 src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs rename src/{Integration.UnitTests/CSharpVB => RoslynAnalyzerServer.UnitTests/Analysis/Configuration}/SonarLintConfigurationXmlSerializerTests.cs (81%) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs rename src/{Integration/CSharpVB/SonarLintConfigXmlSerializer.cs => RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs} (92%) create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs diff --git a/src/Core/CSharpVB/SonarLintConfiguration.cs b/src/Core/CSharpVB/SonarLintConfiguration.cs index 6fe138ff54..0aaa24d738 100644 --- a/src/Core/CSharpVB/SonarLintConfiguration.cs +++ b/src/Core/CSharpVB/SonarLintConfiguration.cs @@ -63,7 +63,7 @@ public class SonarLintConfiguration [XmlArrayItem(ElementName = "Setting")] public List Settings { get; set; } = new List(); - [ XmlArrayItem(ElementName = "Rule")] + [XmlArrayItem(ElementName = "Rule")] public List Rules { get; set; } = new List(); } @@ -73,7 +73,7 @@ public class SonarLintRule public string Key { get; set; } [XmlArrayItem(ElementName = "Parameter")] - public List Parameters { get; set; } = new List(); + public List Parameters { get; set; } = []; } // The SonarLint.xml file has Settings elements and diff --git a/src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs deleted file mode 100644 index ee13590fc5..0000000000 --- a/src/Integration.UnitTests/CSharpVB/RoslynConfigGeneratorTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Integration.CSharpVB; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB; - -[TestClass] -public class RoslynConfigGeneratorTests -{ - private RoslynConfigGenerator testSubject; - private IFileSystemService fileSystemService; - private IGlobalConfigGenerator globalConfigGenerator; - private ISonarLintConfigGenerator sonarLintConfigGenerator; - private ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer; - - private readonly Language language = Language.VBNET; - private const string BaseDirectory = @"C:\base\dir"; - private const string SlconfigDirectory = BaseDirectory + @"\VB"; - private const string SlconfigFilePath = SlconfigDirectory + @"\SonarLint.xml"; - private const string GlobalconfigFilePath = BaseDirectory + @"\sonarlint_vb.globalconfig"; - private readonly Dictionary properties = new() { { "a", "b" } }; - private readonly IFileExclusions fileExclusions = Substitute.For(); - private readonly List ruleStatuses = [Substitute.For()]; - private readonly List ruleParameters = [Substitute.For()]; - - [TestInitialize] - public void TestInitialize() - { - fileSystemService = Substitute.For(); - globalConfigGenerator = Substitute.For(); - sonarLintConfigGenerator = Substitute.For(); - sonarLintConfigurationXmlSerializer = Substitute.For(); - testSubject = new RoslynConfigGenerator(fileSystemService, globalConfigGenerator, sonarLintConfigGenerator, sonarLintConfigurationXmlSerializer); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void GenerateAndSaveConfiguration_CallsGeneratorsWithCorrectParametersAndSaves() - { - var sonarLintConfiguration = new SonarLintConfiguration(); - var slconfigContent = "slconfig content"; - var globalconfigContent = "globalconfig content"; - sonarLintConfigGenerator.Generate(ruleParameters, properties, fileExclusions, language).Returns(sonarLintConfiguration); - sonarLintConfigurationXmlSerializer.Serialize(sonarLintConfiguration).Returns(slconfigContent); - globalConfigGenerator.Generate(ruleStatuses).Returns(globalconfigContent); - - testSubject.GenerateAndSaveConfiguration(language, BaseDirectory, properties, fileExclusions, ruleStatuses, ruleParameters); - - fileSystemService.Directory.Received().CreateDirectory(BaseDirectory); - fileSystemService.Directory.Received().CreateDirectory(SlconfigDirectory); - fileSystemService.File.Received().WriteAllText(SlconfigFilePath, slconfigContent); - fileSystemService.File.Received().WriteAllText(GlobalconfigFilePath, globalconfigContent); - } -} diff --git a/src/Integration/CSharpVB/RoslynConfigGenerator.cs b/src/Integration/CSharpVB/RoslynConfigGenerator.cs index 481ecb8643..d1caf0f2bc 100644 --- a/src/Integration/CSharpVB/RoslynConfigGenerator.cs +++ b/src/Integration/CSharpVB/RoslynConfigGenerator.cs @@ -19,6 +19,7 @@ */ using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; using System.IO; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.CSharpVB; @@ -29,11 +30,11 @@ namespace SonarLint.VisualStudio.Integration.CSharpVB; [Export(typeof(IRoslynConfigGenerator))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] +[ExcludeFromCodeCoverage] // todo https://sonarsource.atlassian.net/browse/SLVS-2420 internal class RoslynConfigGenerator( IFileSystemService fileSystem, IGlobalConfigGenerator globalConfigGenerator, - ISonarLintConfigGenerator sonarLintConfigGenerator, - ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer) + ISonarLintConfigGenerator sonarLintConfigGenerator) : IRoslynConfigGenerator { /// @@ -51,8 +52,6 @@ public void GenerateAndSaveConfiguration( { var roslynGlobalConfig = globalConfigGenerator.Generate(ruleStatuses); Save(roslynGlobalConfig, Path.Combine(baseDirectory, language.SettingsFileNameAndExtension)); - var sonarLintConfiguration = sonarLintConfigGenerator.Generate(ruleParameters, properties, fileExclusions, language); - Save(sonarLintConfigurationXmlSerializer.Serialize(sonarLintConfiguration), Path.Combine(baseDirectory, language.Id, SonarlintConfigFileName)); } private void Save(string config, string filePath) diff --git a/src/Integration.UnitTests/CSharpVB/SonarLintConfigurationXmlSerializerTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintConfigurationXmlSerializerTests.cs similarity index 81% rename from src/Integration.UnitTests/CSharpVB/SonarLintConfigurationXmlSerializerTests.cs rename to src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintConfigurationXmlSerializerTests.cs index fc2027fdd8..39ab9f75a1 100644 --- a/src/Integration.UnitTests/CSharpVB/SonarLintConfigurationXmlSerializerTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintConfigurationXmlSerializerTests.cs @@ -19,15 +19,15 @@ */ using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Integration.CSharpVB; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB; +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; [TestClass] public class SonarLintConfigurationXmlSerializerTests { - private SonarLintConfigurationXmlSerializer testSubject; + private SonarLintConfigurationXmlSerializer testSubject = null!; [TestInitialize] public void TestInitialize() => testSubject = new SonarLintConfigurationXmlSerializer(); @@ -46,10 +46,7 @@ public void Generate_Serialized_ReturnsExpectedXml() Settings = [ new SonarLintKeyValuePair { Key = "sonar.cs.prop1", Value = "value 1" }, - new SonarLintKeyValuePair { Key = "sonar.cs.prop2", Value = "value 2" }, - new SonarLintKeyValuePair { Key = "sonar.exclusions", Value = "**/path1" }, - new SonarLintKeyValuePair { Key = "sonar.global.exclusions", Value = "**/path2" }, - new SonarLintKeyValuePair { Key = "sonar.inclusions", Value = "**/path3" }, + new SonarLintKeyValuePair { Key = "sonar.cs.prop2", Value = "value 2" } ], Rules = [ @@ -69,6 +66,16 @@ public void Generate_Serialized_ReturnsExpectedXml() [ new SonarLintKeyValuePair { Key = "x", Value = "y y" } ] + }, + new SonarLintRule + { + Key = "s777", + Parameters = [] + }, + new SonarLintRule + { + Key = "s888", + Parameters = null } ] }; @@ -88,18 +95,6 @@ public void Generate_Serialized_ReturnsExpectedXml() sonar.cs.prop2 value 2 - - sonar.exclusions - **/path1 - - - sonar.global.exclusions - **/path2 - - - sonar.inclusions - **/path3 - @@ -124,6 +119,13 @@ public void Generate_Serialized_ReturnsExpectedXml() + + s777 + + + + s888 + """); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs new file mode 100644 index 0000000000..125ad3e7cd --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs @@ -0,0 +1,137 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class SonarLintXmlProviderTests +{ + private const string SerializedXml = "serialized xml content"; + private ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer = null!; + private SonarLintXmlProvider testSubject = null!; + private static readonly RoslynRuleConfiguration RuleWithoutParameters = CreateRuleConfig("rule1"); + private static readonly RoslynRuleConfiguration RuleWithParameters = CreateRuleConfig("rule2", parameters: new Dictionary { { "param1", "paramValue1" }, { "param2", "paramValue2" } }); + + [TestInitialize] + public void TestInitialize() + { + sonarLintConfigurationXmlSerializer = Substitute.For(); + sonarLintConfigurationXmlSerializer.Serialize(Arg.Any()).Returns(SerializedXml); + testSubject = new SonarLintXmlProvider(sonarLintConfigurationXmlSerializer); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Create_ReturnsExpectedXml() + { + var profile = new RoslynAnalysisProfile(default, [], []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + result.Path.Should().Be(Path.Combine(Path.GetTempPath(), "SonarLint.xml")); + result.GetText().ToString().Should().Be(SerializedXml); + sonarLintConfigurationXmlSerializer.Received(1).Serialize(Arg.Any()); + } + + [TestMethod] + public void Create_WithMultipleRulesAndProperties_ExpectedConfigurationSerialized() + { + var analysisProperties = new Dictionary { { "prop1", "value1" }, { "prop2", "value2" } }; + var result = testSubject.Create(new RoslynAnalysisProfile(default, [RuleWithoutParameters, RuleWithParameters], analysisProperties)); + + result.Should().NotBeNull(); + + var sonarLintConfiguration = sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0] as SonarLintConfiguration; + sonarLintConfiguration!.Rules.Count.Should().Be(2); + sonarLintConfiguration!.Settings.Should().BeEquivalentTo([ + new SonarLintKeyValuePair { Key = "prop1", Value = "value1" }, + new SonarLintKeyValuePair { Key = "prop2", Value = "value2" } + ]); + } + + [TestMethod] + public void Create_WithRuleNoParametersNoProperties_SerializesCorrectRules() + { + var profile = new RoslynAnalysisProfile(default, [RuleWithoutParameters], []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + var sonarLintConfiguration = sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0] as SonarLintConfiguration; + sonarLintConfiguration.Should().BeEquivalentTo(new SonarLintConfiguration { Rules = [new SonarLintRule { Key = "rule1", Parameters = [] }], Settings = [] }); + } + + [TestMethod] + public void Create_WithRuleWithParameters_SerializesCorrectRules() + { + var profile = new RoslynAnalysisProfile(default, [RuleWithParameters], []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + var sonarLintConfiguration = sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0] as SonarLintConfiguration; + sonarLintConfiguration.Should().BeEquivalentTo(new SonarLintConfiguration + { + Rules = + [ + new SonarLintRule + { + Key = "rule2", Parameters = [new SonarLintKeyValuePair { Key = "param1", Value = "paramValue1" }, new SonarLintKeyValuePair { Key = "param2", Value = "paramValue2" }] + } + ], + Settings = [] + }); + } + + [TestMethod] + public void Create_WithInactiveRules_OnlyIncludesActiveRules() + { + const string inactiveRuleKey = "inactiveRule"; + var rules = new List { RuleWithoutParameters, CreateRuleConfig(inactiveRuleKey, false), RuleWithParameters }; + var profile = new RoslynAnalysisProfile(default, rules, []); + + var result = testSubject.Create(profile); + + result.Should().NotBeNull(); + var sonarLintConfiguration = (SonarLintConfiguration)sonarLintConfigurationXmlSerializer.ReceivedCalls().Single().GetArguments()[0]!; + sonarLintConfiguration.Rules.Count.Should().Be(2); + sonarLintConfiguration.Rules.Select(x => x.Key).Should().NotContain(inactiveRuleKey); + } + + private static RoslynRuleConfiguration CreateRuleConfig( + string ruleKey, + bool isActive = true, + Dictionary? parameters = null) => + new( + new SonarCompositeRuleId(Language.CSharp.RepoInfo.Key, ruleKey), + isActive, + parameters); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs new file mode 100644 index 0000000000..f8a830b5e2 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/ISonarLintConfigurationXmlSerializer.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.CSharpVB; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface ISonarLintConfigurationXmlSerializer +{ + string Serialize(SonarLintConfiguration configuration); +} diff --git a/src/Integration/CSharpVB/SonarLintConfigXmlSerializer.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs similarity index 92% rename from src/Integration/CSharpVB/SonarLintConfigXmlSerializer.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs index 37ff4a0973..acd06133ed 100644 --- a/src/Integration/CSharpVB/SonarLintConfigXmlSerializer.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintConfigurationXmlSerializer.cs @@ -25,12 +25,7 @@ using System.Xml.Serialization; using SonarLint.VisualStudio.Core.CSharpVB; -namespace SonarLint.VisualStudio.Integration.CSharpVB; - -internal interface ISonarLintConfigurationXmlSerializer -{ - string Serialize(SonarLintConfiguration configuration); -} +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [Export(typeof(ISonarLintConfigurationXmlSerializer))] [PartCreationPolicy(CreationPolicy.Shared)] diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs new file mode 100644 index 0000000000..532b5b2a2a --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/SonarLintXmlProvider.cs @@ -0,0 +1,50 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.IO; +using SonarLint.VisualStudio.Core.CSharpVB; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(ISonarLintXmlProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class SonarLintXmlProvider(ISonarLintConfigurationXmlSerializer sonarLintConfigurationXmlSerializer) : ISonarLintXmlProvider +{ + public SonarLintXmlConfigurationFile Create(RoslynAnalysisProfile analysisProfile) + { + var sonarLintConfiguration = new SonarLintConfiguration + { + Settings = ConvertDictionary(analysisProfile.AnalysisProperties), + Rules = analysisProfile + .Rules + .Where(x => x.IsActive) + .Select(x => new SonarLintRule { Key = x.RuleId.RuleKey, Parameters = ConvertDictionary(x.Parameters) }) + .ToList() + }; + + return new SonarLintXmlConfigurationFile(Path.GetTempPath(), sonarLintConfigurationXmlSerializer.Serialize(sonarLintConfiguration)); + } + + private static List? ConvertDictionary(Dictionary? dictionary) => dictionary?.Select(ConvertKeyValuePair).ToList() ?? []; + + private static SonarLintKeyValuePair ConvertKeyValuePair(KeyValuePair x) => new() { Key = x.Key, Value = x.Value }; +} From 2a2582390f125a085d40f6f4d6bcc319f71cc610 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 22 Aug 2025 13:56:17 +0000 Subject: [PATCH 12/38] SLVS-2417 Make sure raised roslyn issues are no longer filtered out (#6385) --- src/Core.UnitTests/LanguageTests.cs | 20 +++- src/Core.UnitTests/PluginInfoTests.cs | 20 ++++ src/Core/Language.cs | 21 ++-- src/Core/PluginInfo.cs | 5 +- src/EmbeddedSonarAnalyzer.props | 2 +- ...> SLCoreEmbeddedPluginJarProviderTests.cs} | 32 +++--- ...tor.cs => SLCoreEmbeddedPluginProvider.cs} | 23 +++-- .../Http/Helper/HttpRequester.cs | 2 +- .../SLCoreTestRunner.cs | 4 +- .../Analysis/RaisedFindingProcessorTests.cs | 99 +++---------------- .../Analysis/RaisedFindingProcessor.cs | 21 +--- .../SlCoreLanguageProviderTests.cs | 15 --- .../SLCoreInstanceFactoryTests.cs | 6 +- .../SLCoreInstanceHandleTests.cs | 29 +++--- ...or.cs => ISLCoreEmbeddedPluginProvider.cs} | 6 +- .../Configuration/ISLCoreLanguageProvider.cs | 6 -- src/SLCore/ISLCoreInstanceFactory.cs | 8 +- src/SLCore/ISLCoreInstanceHandle.cs | 9 +- 18 files changed, 141 insertions(+), 187 deletions(-) rename src/Integration.Vsix.UnitTests/SLCore/{SLCoreEmbeddedPluginJarLocatorTests.cs => SLCoreEmbeddedPluginJarProviderTests.cs} (87%) rename src/Integration.Vsix/SLCore/{SLCoreEmbeddedPluginJarLocator.cs => SLCoreEmbeddedPluginProvider.cs} (75%) rename src/SLCore/Configuration/{ISLCoreEmbeddedPluginJarLocator.cs => ISLCoreEmbeddedPluginProvider.cs} (91%) diff --git a/src/Core.UnitTests/LanguageTests.cs b/src/Core.UnitTests/LanguageTests.cs index cd9c82eef2..d43177ce07 100644 --- a/src/Core.UnitTests/LanguageTests.cs +++ b/src/Core.UnitTests/LanguageTests.cs @@ -54,6 +54,9 @@ public void Language_Ctor_ArgChecks() act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo: null, settingsFileName: fileSuffix); act.Should().NotThrow(); + + act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, additionalPlugins: null); + act.Should().NotThrow(); } [TestMethod] @@ -133,8 +136,10 @@ public void HasRepoKey_RepoNorSecurityRepoHasKey_ReturnsFalse() [TestMethod] public void Language_HasCorrectPlugin() { - LanguageHasExpectedPlugin(Language.CSharp, "csharpenterprise", "sonar-csharp-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); - LanguageHasExpectedPlugin(Language.VBNET, "vbnetenterprise", "sonar-vbnet-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); + LanguageHasExpectedPlugin(Language.CSharp, "sqvsroslyn", "sonarqube-ide-visualstudio-roslyn-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); + LanguageHasExpectedAdditionalPlugins(Language.CSharp, [new("csharpenterprise", "sonar-csharp-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar", isEnabledForAnalysis: false)]); + LanguageHasExpectedPlugin(Language.VBNET, "sqvsroslyn", "sonarqube-ide-visualstudio-roslyn-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); + LanguageHasExpectedAdditionalPlugins(Language.VBNET, [new("vbnetenterprise", "sonar-vbnet-enterprise-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar", isEnabledForAnalysis: false)]); LanguageHasExpectedPlugin(Language.Cpp, "cpp", "sonar-cfamily-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); LanguageHasExpectedPlugin(Language.C, "cpp", "sonar-cfamily-plugin-(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.jar"); @@ -171,6 +176,17 @@ private static void LanguageHasExpectedPlugin(Language language, string pluginKe { language.PluginInfo.Key.Should().Be(pluginKey); language.PluginInfo.FilePattern.Should().Be(filePattern); + language.PluginInfo.IsEnabledForAnalysis.Should().Be(true); + } + + private static void LanguageHasExpectedAdditionalPlugins(Language language, List expectedPlugins) + { + foreach (var expectedPlugin in expectedPlugins) + { + language.AdditionalPlugins.Should().Contain(x => x.Key == expectedPlugin.Key && + x.FilePattern == expectedPlugin.FilePattern && + x.IsEnabledForAnalysis == expectedPlugin.IsEnabledForAnalysis); + } } private static void LanguageHasExpectedRepoInfo(Language language, string repoKey, string folderName) => HasExpectedRepoInfo(language.RepoInfo, repoKey, folderName); diff --git a/src/Core.UnitTests/PluginInfoTests.cs b/src/Core.UnitTests/PluginInfoTests.cs index 3a221ee95d..1a1b092f80 100644 --- a/src/Core.UnitTests/PluginInfoTests.cs +++ b/src/Core.UnitTests/PluginInfoTests.cs @@ -16,4 +16,24 @@ public void PluginInfo_NullFilePattern_DoesNotThrow() Action act = () => new PluginInfo("csharp", null); act.Should().NotThrow(); } + + [TestMethod] + [DataRow("csharp", "csharp-plugin-(\\d+\\).jar", true)] + [DataRow("vbnet", "vbnet-plugin-(\\d+\\).jar", false)] + public void PluginInfo_InitializesParameters(string key, string pattern, bool isEnabled) + { + var pluginInfo = new PluginInfo(key, pattern, isEnabled); + + pluginInfo.Key.Should().Be(key); + pluginInfo.FilePattern.Should().Be(pattern); + pluginInfo.IsEnabledForAnalysis.Should().Be(isEnabled); + } + + [TestMethod] + public void PluginInfo_IsEnabledDefaultsToTrue() + { + var pluginInfo = new PluginInfo("csharp", "csharp-plugin-(\\d+\\).jar"); + + pluginInfo.IsEnabledForAnalysis.Should().Be(true); + } } diff --git a/src/Core/Language.cs b/src/Core/Language.cs index 1eea8d9b55..e5a6218529 100644 --- a/src/Core/Language.cs +++ b/src/Core/Language.cs @@ -38,8 +38,9 @@ namespace SonarLint.VisualStudio.Core public sealed class Language : IEquatable { private const string VersionNumberPattern = "(\\d+\\.\\d+\\.\\d+\\.\\d+)\\"; - private static readonly PluginInfo CSharpPlugin = new("csharpenterprise", $"sonar-csharp-enterprise-plugin-{VersionNumberPattern}.jar"); - private static readonly PluginInfo VbNetPlugin = new("vbnetenterprise", $"sonar-vbnet-enterprise-plugin-{VersionNumberPattern}.jar"); + private static readonly PluginInfo SqvsRoslynPlugin = new("sqvsroslyn", $"sonarqube-ide-visualstudio-roslyn-plugin-{VersionNumberPattern}.jar"); + private static readonly PluginInfo CSharpPlugin = new("csharpenterprise", $"sonar-csharp-enterprise-plugin-{VersionNumberPattern}.jar", isEnabledForAnalysis: false); + private static readonly PluginInfo VbNetPlugin = new("vbnetenterprise", $"sonar-vbnet-enterprise-plugin-{VersionNumberPattern}.jar", isEnabledForAnalysis: false); private static readonly PluginInfo SecretsPlugin = new("text", $"sonar-text-plugin-{VersionNumberPattern}.jar"); private static readonly PluginInfo CFamilyPlugin = new("cpp", $"sonar-cfamily-plugin-{VersionNumberPattern}.jar"); private static readonly PluginInfo JavascriptPlugin = new("javascript", $"sonar-javascript-plugin-{VersionNumberPattern}.jar"); @@ -62,9 +63,10 @@ public sealed class Language : IEquatable private static readonly RepoInfo TsqlRepo = new("tsql"); public static readonly Language Unknown = new(); - public static readonly Language CSharp = new("CSharp", CoreStrings.CSharpLanguageName, "cs", CSharpPlugin, CSharpRepo, CSharpSecurityRepo, - settingsFileName: "sonarlint_csharp.globalconfig"); - public static readonly Language VBNET = new("VB", CoreStrings.VBNetLanguageName, "vbnet", VbNetPlugin, VbNetRepo, settingsFileName: "sonarlint_vb.globalconfig"); + public static readonly Language CSharp = new("CSharp", CoreStrings.CSharpLanguageName, "cs", SqvsRoslynPlugin, CSharpRepo, CSharpSecurityRepo, + settingsFileName: "sonarlint_csharp.globalconfig", additionalPlugins: [CSharpPlugin]); + public static readonly Language VBNET = new("VB", CoreStrings.VBNetLanguageName, "vbnet", SqvsRoslynPlugin, VbNetRepo, settingsFileName: "sonarlint_vb.globalconfig", + additionalPlugins: [VbNetPlugin]); public static readonly Language Cpp = new("C++", CoreStrings.CppLanguageName, "cpp", CFamilyPlugin, CppRepo); public static readonly Language C = new("C", "C", "c", CFamilyPlugin, CRepo); public static readonly Language Js = new("Js", "JavaScript", "js", JavascriptPlugin, JsRepo, JsSecurityRepo); @@ -101,6 +103,11 @@ public sealed class Language : IEquatable /// e.g. for ruleset-based languages this will be a language identifier + ".globalconfig" public string SettingsFileNameAndExtension { get; } + /// + /// Additional plugins that should be installed for a language + /// + public PluginInfo[] AdditionalPlugins { get; } + public RepoInfo RepoInfo { get; } /// @@ -125,7 +132,8 @@ public Language( PluginInfo pluginInfo, RepoInfo repoInfo, RepoInfo securityRepoInfo = null, - string settingsFileName = null) + string settingsFileName = null, + PluginInfo[] additionalPlugins = null) { if (string.IsNullOrWhiteSpace(id)) { @@ -140,6 +148,7 @@ public Language( Id = id; Name = name; SettingsFileNameAndExtension = settingsFileName; + AdditionalPlugins = additionalPlugins; ServerLanguageKey = serverLanguageKey ?? throw new ArgumentNullException(nameof(serverLanguageKey)); PluginInfo = pluginInfo ?? throw new ArgumentNullException(nameof(pluginInfo)); RepoInfo = repoInfo ?? throw new ArgumentNullException(nameof(repoInfo)); diff --git a/src/Core/PluginInfo.cs b/src/Core/PluginInfo.cs index 3d8460077f..0b095c2ad4 100644 --- a/src/Core/PluginInfo.cs +++ b/src/Core/PluginInfo.cs @@ -22,10 +22,11 @@ namespace SonarLint.VisualStudio.Core; public record PluginInfo { - public PluginInfo(string pluginKey, string filePattern) + public PluginInfo(string pluginKey, string filePattern, bool isEnabledForAnalysis = true) { Key = pluginKey ?? throw new ArgumentNullException(nameof(pluginKey)); FilePattern = filePattern; + IsEnabledForAnalysis = isEnabledForAnalysis; } public string Key { get; } @@ -34,4 +35,6 @@ public PluginInfo(string pluginKey, string filePattern) /// Nullable, because it can be a connected mode only language so there will be no file on disk (plugin won't be embedded) /// public string FilePattern { get; } + + public bool IsEnabledForAnalysis { get; } } diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 0f8193d30e..c389246465 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0 + 1.0.0.0 10.34.0.83431 1.0.0 diff --git a/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarLocatorTests.cs b/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarProviderTests.cs similarity index 87% rename from src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarLocatorTests.cs rename to src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarProviderTests.cs index 4a27e6e14a..7424f07da9 100644 --- a/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/SLCore/SLCoreEmbeddedPluginJarProviderTests.cs @@ -29,13 +29,13 @@ namespace SonarLint.VisualStudio.Integration.Vsix.UnitTests.SLCore; [TestClass] -public class SLCoreEmbeddedPluginJarLocatorTests +public class SLCoreEmbeddedPluginJarProviderTests { private IDirectory directory; private IFileSystem fileSystem; private ILanguageProvider languageProvider; private ILogger logger; - private SLCoreEmbeddedPluginJarLocator testSubject; + private SLCoreEmbeddedPluginProvider testSubject; private IVsixRootLocator vsixRootLocator; [TestInitialize] @@ -46,7 +46,7 @@ public void TestInitialize() logger = Substitute.For(); directory = Substitute.For(); MockLanguageProvider(); - testSubject = new SLCoreEmbeddedPluginJarLocator(vsixRootLocator, fileSystem, logger, languageProvider); + testSubject = new SLCoreEmbeddedPluginProvider(vsixRootLocator, fileSystem, logger, languageProvider); MockVsixLocator(); MockFileSystem(); @@ -54,13 +54,13 @@ public void TestInitialize() [TestMethod] public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(languageProvider)); [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] public void ListJarFiles_DirectoryNotExists_ReturnsEmpty() @@ -157,19 +157,29 @@ public void ListConnectedModeEmbeddedPluginPathsByKey_NoJars_ReturnsEmptyDiction var result = testSubject.ListConnectedModeEmbeddedPluginPathsByKey(); result.Should().BeEmpty(); - logger.Received(4).LogVerbose(Strings.ConnectedModeEmbeddedPluginJarLocator_JarsNotFound); + logger.Received(testSubject.StandalonePlugins.Count).LogVerbose(Strings.ConnectedModeEmbeddedPluginJarLocator_JarsNotFound); } [TestMethod] - public void StandalonePlugins_ReturnsStandalonePluginsExceptRoslyn() + public void StandalonePlugins_ReturnsStandalonePluginsIncludingRoslyn() { _ = languageProvider.Received(1).LanguagesInStandaloneMode; - _ = languageProvider.Received(1).RoslynLanguages; + _ = languageProvider.DidNotReceive().RoslynLanguages; - var expectedPlugins = languageProvider.LanguagesInStandaloneMode.Except(languageProvider.RoslynLanguages).Select(x => x.PluginInfo).ToHashSet(); + var expectedPlugins = languageProvider.LanguagesInStandaloneMode.Select(x => x.PluginInfo).ToHashSet(); testSubject.StandalonePlugins.Should().BeEquivalentTo(expectedPlugins); } + [TestMethod] + public void ListDisabledPluginKeysForAnalysis_ReturnsCsharpAndVbNetPluginKeys() + { + List expectedPluginKeys = ["csharpenterprise", "vbnetenterprise"]; + + var result = testSubject.ListDisabledPluginKeysForAnalysis(); + + result.Should().BeEquivalentTo(expectedPluginKeys); + } + private void MockFileSystem(bool directoryExists, params string[] files) { directory.Exists(default).ReturnsForAnyArgs(false); @@ -198,8 +208,8 @@ private void MockLanguageProvider() languageProvider = Substitute.For(); // doesn't have to be the complete list for testing purposes languageProvider.LanguagesInStandaloneMode.Returns([ - Language.CSharp, Language.C, Language.Js, Language.Css, Language.Html, Language.Secrets + Language.CSharp, Language.VBNET, Language.C, Language.Js, Language.Css, Language.Html, Language.Secrets ]); - languageProvider.RoslynLanguages.Returns([Language.CSharp]); + languageProvider.RoslynLanguages.Returns([Language.CSharp, Language.VBNET,]); } } diff --git a/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginJarLocator.cs b/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginProvider.cs similarity index 75% rename from src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginJarLocator.cs rename to src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginProvider.cs index 1984bc620f..dafc53336e 100644 --- a/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginJarLocator.cs +++ b/src/Integration.Vsix/SLCore/SLCoreEmbeddedPluginProvider.cs @@ -26,25 +26,28 @@ using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.Integration.Vsix.Resources; using SonarLint.VisualStudio.SLCore.Configuration; +using IFileSystem = System.IO.Abstractions.IFileSystem; +using ILogger = SonarLint.VisualStudio.Core.ILogger; namespace SonarLint.VisualStudio.Integration.Vsix.SLCore; -[Export(typeof(ISLCoreEmbeddedPluginJarLocator))] +[Export(typeof(ISLCoreEmbeddedPluginProvider))] [PartCreationPolicy(CreationPolicy.Shared)] -public class SLCoreEmbeddedPluginJarLocator : ISLCoreEmbeddedPluginJarLocator +public class SLCoreEmbeddedPluginProvider : ISLCoreEmbeddedPluginProvider { private const string JarFolderName = "DownloadedJars"; private readonly IFileSystem fileSystem; private readonly ILogger logger; + private readonly ILanguageProvider languageProvider; private readonly IVsixRootLocator vsixRootLocator; internal HashSet StandalonePlugins { get; } [ImportingConstructor] - public SLCoreEmbeddedPluginJarLocator(IVsixRootLocator vsixRootLocator, ILogger logger, ILanguageProvider languageProvider) : this(vsixRootLocator, new FileSystem(), logger, languageProvider) { } + public SLCoreEmbeddedPluginProvider(IVsixRootLocator vsixRootLocator, ILogger logger, ILanguageProvider languageProvider) : this(vsixRootLocator, new FileSystem(), logger, languageProvider) { } - internal SLCoreEmbeddedPluginJarLocator( + internal SLCoreEmbeddedPluginProvider( IVsixRootLocator vsixRootLocator, IFileSystem fileSystem, ILogger logger, @@ -53,7 +56,8 @@ internal SLCoreEmbeddedPluginJarLocator( this.vsixRootLocator = vsixRootLocator; this.fileSystem = fileSystem; this.logger = logger; - StandalonePlugins = languageProvider.LanguagesInStandaloneMode.Except(languageProvider.RoslynLanguages).Select(x => x.PluginInfo).ToHashSet(); + this.languageProvider = languageProvider; + StandalonePlugins = languageProvider.LanguagesInStandaloneMode.Select(x => x.PluginInfo).ToHashSet(); } public List ListJarFiles() @@ -83,9 +87,16 @@ public Dictionary ListConnectedModeEmbeddedPluginPathsByKey() return connectedModeEmbeddedPluginPathsByKey; } + public List ListDisabledPluginKeysForAnalysis() + { + var allPlugins = languageProvider.LanguagesInStandaloneMode.Where(x => x.AdditionalPlugins != null).SelectMany(x => x.AdditionalPlugins).ToHashSet(); + allPlugins.UnionWith(StandalonePlugins); + return allPlugins.Where(p => !p.IsEnabledForAnalysis).Select(p => p.Key).ToList(); + } + private string GetPathByPluginKey(List pluginFilePaths, string pluginKey, string pluginNameRegexPattern) { - var regex = new Regex(pluginNameRegexPattern); + var regex = new Regex(pluginNameRegexPattern, RegexOptions.None, RegexConstants.DefaultTimeout); var matchedFilePaths = pluginFilePaths.Where(jar => regex.IsMatch(jar)).ToList(); switch (matchedFilePaths.Count) { diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs index 239636cc4d..1f87c6010a 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs @@ -12,7 +12,7 @@ internal record AnalysisRequestConfig(SecureString Token, string RequestUri, par internal sealed class HttpRequester : IDisposable { - private const int WaitForServerMsTimeout = 500; + private const int WaitForServerMsTimeout = 2000; private const string JsonMediaType = "application/json"; private const string XAuthTokenHeader = "X-Auth-Token"; private readonly HttpClient httpClient; diff --git a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs index 255990428c..70fa9273e9 100644 --- a/src/SLCore.IntegrationTests/SLCoreTestRunner.cs +++ b/src/SLCore.IntegrationTests/SLCoreTestRunner.cs @@ -110,8 +110,9 @@ public async Task Start(TestLogger testLogger) var connectionProvider = Substitute.For(); connectionProvider.GetServerConnections().Returns(new Dictionary()); - var jarProvider = Substitute.For(); + var jarProvider = Substitute.For(); jarProvider.ListJarFiles().Returns(DependencyLocator.AnalyzerPlugins); + jarProvider.ListDisabledPluginKeysForAnalysis().Returns([Language.CSharp.AdditionalPlugins.Single().Key, Language.VBNET.AdditionalPlugins.Single().Key]); var compatibleNodeLocator = Substitute.For(); compatibleNodeLocator.Get().Returns((string)null); @@ -172,7 +173,6 @@ private static void SetLanguagesConfigurationToDefaults(ISLCoreLanguageProvider { var defaultLanguageProvider = new SLCoreLanguageProvider(LanguageProvider.Instance); languageProvider.LanguagesInStandaloneMode.Returns(defaultLanguageProvider.LanguagesInStandaloneMode); - languageProvider.LanguagesWithDisabledAnalysis.Returns(defaultLanguageProvider.LanguagesWithDisabledAnalysis); } public void Dispose() diff --git a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs index c45a9b4f76..55cef4e5d7 100644 --- a/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs +++ b/src/SLCore.Listeners.UnitTests/Implementation/Analysis/RaisedFindingProcessorTests.cs @@ -21,13 +21,11 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.SLCore.Common.Models; -using SonarLint.VisualStudio.SLCore.Configuration; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; using SonarLint.VisualStudio.SLCore.Listeners.Implementation.Analysis; using SonarLint.VisualStudio.SLCore.Protocol; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; -using SloopLanguage = SonarLint.VisualStudio.SLCore.Common.Models.Language; namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests.Implementation.Analysis; @@ -41,7 +39,6 @@ public class RaisedFindingProcessorTests [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -60,7 +57,7 @@ public void RaiseFindings_AnalysisIdIsNull_DoesNotIgnore() testSubject.RaiseFinding(raiseFindingParams, publisher); - raiseFindingParamsToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => !x.Any())); + raiseFindingParamsToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => x.Count() == 2)); publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); } @@ -82,57 +79,6 @@ public void RaiseFindings_NoFindings_Ignores() publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); } - [TestMethod] - public void RaiseFindings_NoKnownLanguages_PublishesEmpty() - { - var analysisId = Guid.NewGuid(); - var isIntermediatePublication = false; - var raiseFindingParams = new RaiseFindingParams("CONFIGURATION_ID", twoFindingsByFileUri, isIntermediatePublication, analysisId); - var publisher = CreatePublisher(); - IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = CreateConverter(twoFindingsByFileUri.Single().Key, [], []); - - var analysisStatusNotifierFactory = CreateAnalysisStatusNotifierFactory(out var analysisStatusNotifier, fileUri.LocalPath); - var constantsProvider = CreateConstantsProviderWithLanguages([]); - - var testSubject = CreateTestSubject(raiseFindingToAnalysisIssueConverter: raiseFindingToAnalysisIssueConverter, - analysisStatusNotifierFactory: analysisStatusNotifierFactory, - slCoreConstantsProvider: constantsProvider); - - testSubject.RaiseFinding(raiseFindingParams, publisher); - - raiseFindingToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => !x.Any())); - publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); - analysisStatusNotifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); - analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 0, FindingsType, isIntermediatePublication); - VerifyCorrectConstantsAreUsed(constantsProvider); - } - - [TestMethod] - public void RaiseFindings_NoSupportedLanguages_PublishesEmpty() - { - var analysisId = Guid.NewGuid(); - var isIntermediatePublication = false; - var raiseFindingParams = new RaiseFindingParams("CONFIGURATION_ID", twoFindingsByFileUri, isIntermediatePublication, analysisId); - var publisher = CreatePublisher(); - IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = CreateConverter(twoFindingsByFileUri.Single().Key, [], []); - - var analysisStatusNotifierFactory = CreateAnalysisStatusNotifierFactory(out var analysisStatusNotifier, fileUri.LocalPath); - var constantsProvider = CreateConstantsProviderWithLanguages([SloopLanguage.JAVA]); - - var testSubject = CreateTestSubject( - raiseFindingToAnalysisIssueConverter: raiseFindingToAnalysisIssueConverter, - analysisStatusNotifierFactory: analysisStatusNotifierFactory, - slCoreConstantsProvider: constantsProvider); - - testSubject.RaiseFinding(raiseFindingParams, publisher); - - raiseFindingToAnalysisIssueConverter.Received().GetAnalysisIssues(fileUri, Arg.Is>(x => !x.Any())); - publisher.Received().Publish(fileUri.LocalPath, Arg.Is>(x => !x.Any())); - analysisStatusNotifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); - analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 0, FindingsType, isIntermediatePublication); - VerifyCorrectConstantsAreUsed(constantsProvider); - } - [TestMethod] public void RaiseFindings_HasNoFileUri_FinishesAnalysis() { @@ -157,40 +103,37 @@ public void RaiseFindings_PublishFindings(bool isIntermediate) { var analysisId = Guid.NewGuid(); var analysisIssue1 = CreateAnalysisIssue("csharpsquid:S100"); - var analysisIssue2 = CreateAnalysisIssue("secrets:S1012"); - var filteredIssues = new[] { analysisIssue1, analysisIssue2 }; + var analysisIssue2 = CreateAnalysisIssue("javascript:S101"); + var analysisIssue3 = CreateAnalysisIssue("secrets:S1012"); + var raisedIssues = new[] { analysisIssue1, analysisIssue2, analysisIssue3 }; var raisedFinding1 = CreateTestFinding("csharpsquid:S100"); var raisedFinding2 = CreateTestFinding("javascript:S101"); var raisedFinding3 = CreateTestFinding("secrets:S1012"); - var filteredRaisedFindings = new[] { raisedFinding1, raisedFinding3 }; - var raisedFindings = new List { raisedFinding1, raisedFinding2, raisedFinding3 }; + var raisedFindingDtos = new List { raisedFinding1, raisedFinding2, raisedFinding3 }; - var findingsByFileUri = new Dictionary> { { fileUri, raisedFindings } }; + var findingsByFileUri = new Dictionary> { { fileUri, raisedFindingDtos } }; var raiseFindingParams = new RaiseFindingParams("CONFIGURATION_ID", findingsByFileUri, isIntermediate, analysisId); var publisher = CreatePublisher(); IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = - CreateConverter(findingsByFileUri.Single().Key, filteredRaisedFindings, filteredIssues); + CreateConverter(findingsByFileUri.Single().Key, raisedFindingDtos, raisedIssues); var analysisStatusNotifierFactory = CreateAnalysisStatusNotifierFactory(out var analysisStatusNotifier, fileUri.LocalPath); - var constantsProvider = CreateConstantsProviderWithLanguages(SloopLanguage.SECRETS, SloopLanguage.CS); var testSubject = CreateTestSubject( raiseFindingToAnalysisIssueConverter: raiseFindingToAnalysisIssueConverter, - analysisStatusNotifierFactory: analysisStatusNotifierFactory, - slCoreConstantsProvider: constantsProvider); + analysisStatusNotifierFactory: analysisStatusNotifierFactory); testSubject.RaiseFinding(raiseFindingParams, publisher); - publisher.Received(1).Publish(fileUri.LocalPath, filteredIssues); + publisher.Received(1).Publish(fileUri.LocalPath, raisedIssues); raiseFindingToAnalysisIssueConverter.Received(1).GetAnalysisIssues(findingsByFileUri.Single().Key, Arg.Is>( - x => x.SequenceEqual(filteredRaisedFindings))); + x => x.SequenceEqual(raisedFindingDtos))); analysisStatusNotifierFactory.Received(1).Create([fileUri.LocalPath]); analysisStatusNotifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); - analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 2, FindingsType, isIntermediate); - VerifyCorrectConstantsAreUsed(constantsProvider); + analysisStatusNotifier.Received().AnalysisProgressed(analysisId, 3, FindingsType, isIntermediate); } [DataRow(true)] @@ -241,11 +184,8 @@ public void RaiseFindings_MultipleFiles_PublishFindingsForEachFile(bool isInterm private RaisedFindingProcessor CreateTestSubject( IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter = null, IAnalysisStatusNotifierFactory analysisStatusNotifierFactory = null, - ILogger logger = null, - ISLCoreLanguageProvider slCoreConstantsProvider = null) => + ILogger logger = null) => new( - slCoreConstantsProvider ?? - CreateConstantsProviderWithLanguages(SloopLanguage.SECRETS, SloopLanguage.JS, SloopLanguage.TS, SloopLanguage.CSS), raiseFindingToAnalysisIssueConverter ?? Substitute.For(), analysisStatusNotifierFactory ?? Substitute.For(), logger ?? new TestLogger()); @@ -260,13 +200,6 @@ private static IRaiseFindingToAnalysisIssueConverter CreateConverter( return raiseFindingParamsToAnalysisIssueConverter; } - private ISLCoreLanguageProvider CreateConstantsProviderWithLanguages(params SloopLanguage[] languages) - { - var slCoreLanguageProvider = Substitute.For(); - slCoreLanguageProvider.AllAnalyzableLanguages.Returns(languages.ToList()); - return slCoreLanguageProvider; - } - private IAnalysisStatusNotifierFactory CreateAnalysisStatusNotifierFactory( out IAnalysisStatusNotifier analysisStatusNotifier, string filePath) @@ -304,14 +237,6 @@ private static IAnalysisIssue CreateAnalysisIssue(string ruleKey) return analysisIssue1; } - private static void VerifyCorrectConstantsAreUsed(ISLCoreLanguageProvider languageProvider) - { - _ = languageProvider.DidNotReceive().LanguagesInStandaloneMode; - _ = languageProvider.DidNotReceive().ExtraLanguagesInConnectedMode; - _ = languageProvider.DidNotReceive().LanguagesWithDisabledAnalysis; - _ = languageProvider.Received().AllAnalyzableLanguages; - } - private record TestFinding( Guid id, string serverKey, diff --git a/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs b/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs index 8f6c8cefac..8402e8a3af 100644 --- a/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs +++ b/src/SLCore.Listeners/Implementation/Analysis/RaisedFindingProcessor.cs @@ -21,8 +21,6 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.SLCore.Common.Helpers; -using SonarLint.VisualStudio.SLCore.Configuration; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; @@ -37,14 +35,11 @@ internal interface IRaisedFindingProcessor [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] internal class RaisedFindingProcessor( - ISLCoreLanguageProvider slCoreLanguageProvider, IRaiseFindingToAnalysisIssueConverter raiseFindingToAnalysisIssueConverter, IAnalysisStatusNotifierFactory analysisStatusNotifierFactory, ILogger logger) : IRaisedFindingProcessor { - private readonly List analyzableLanguagesRuleKeyPrefixes = CalculateAnalyzableRulePrefixes(slCoreLanguageProvider); - public void RaiseFinding(RaiseFindingParams parameters, IFindingsPublisher findingsPublisher) where T : RaisedFindingDto { if (!IsValid(parameters)) @@ -74,20 +69,10 @@ private void PublishFindings(RaiseFindingParams parameters, IFindingsPubli var fileUri = fileAndIssues.Key; var localPath = fileUri.LocalPath; var analysisStatusNotifier = analysisStatusNotifierFactory.Create([localPath]); - var supportedRaisedIssues = GetSupportedLanguageFindings(fileAndIssues.Value ?? []); + var raisedIssues = fileAndIssues.Value ?? []; findingsPublisher.Publish(localPath, - raiseFindingToAnalysisIssueConverter.GetAnalysisIssues(fileUri, supportedRaisedIssues)); - analysisStatusNotifier.AnalysisProgressed(parameters.analysisId, supportedRaisedIssues.Length, findingsPublisher.FindingsType, parameters.isIntermediatePublication); + raiseFindingToAnalysisIssueConverter.GetAnalysisIssues(fileUri, raisedIssues)); + analysisStatusNotifier.AnalysisProgressed(parameters.analysisId, raisedIssues.Count, findingsPublisher.FindingsType, parameters.isIntermediatePublication); } } - - private T[] GetSupportedLanguageFindings(IEnumerable findings) where T : RaisedFindingDto => - findings.Where(i => analyzableLanguagesRuleKeyPrefixes.Exists(languageRepo => i.ruleKey.StartsWith(languageRepo))).ToArray(); - - private static List CalculateAnalyzableRulePrefixes(ISLCoreLanguageProvider slCoreConstantsProvider) => - slCoreConstantsProvider.AllAnalyzableLanguages? - .Select(x => x.ConvertToCoreLanguage()) - .Select(x => x.RepoInfo?.Key) - .Where(r => r is not null) - .ToList() ?? []; } diff --git a/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs b/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs index 486d44d2b0..2a63b3179d 100644 --- a/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs +++ b/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs @@ -57,21 +57,6 @@ public void ExtraLanguagesInConnectedMode_ShouldBeExpected() testSubject.ExtraLanguagesInConnectedMode.Should().BeEquivalentTo(languageProvider.ExtraLanguagesInConnectedMode.Select(x => x.ConvertToSlCoreLanguage())); } - [TestMethod] - public void LanguagesWithDisabledAnalysis_ShouldBeExpected() - { - _ = languageProvider.Received(1).RoslynLanguages; - testSubject.LanguagesWithDisabledAnalysis.Should().BeEquivalentTo(languageProvider.RoslynLanguages.Select(x => x.ConvertToSlCoreLanguage())); - } - - [TestMethod] - public void AllAnalyzableLanguages_ShouldBeExpected() - { - var expected = testSubject.LanguagesInStandaloneMode.Concat(testSubject.ExtraLanguagesInConnectedMode).Except(testSubject.LanguagesWithDisabledAnalysis); - - testSubject.AllAnalyzableLanguages.Should().BeEquivalentTo(expected); - } - private void MockLanguageProvider() { languageProvider = Substitute.For(); diff --git a/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs b/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs index aea11c11dc..b84cf49a5c 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceFactoryTests.cs @@ -43,7 +43,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), @@ -68,7 +68,7 @@ public void CreateInstance_ReturnsNonNull() var slCoreLanguageProvider = Substitute.For(); var islCoreFoldersProvider = Substitute.For(); var serverConnectionsProvider = Substitute.For(); - var islCoreEmbeddedPluginJarLocator = Substitute.For(); + var slCoreEmbeddedPluginProvider = Substitute.For(); var compatibleNodeLocator = Substitute.For(); var esLintBridgeLocator = Substitute.For(); var activeSolutionBoundTracker = Substitute.For(); @@ -84,7 +84,7 @@ public void CreateInstance_ReturnsNonNull() slCoreLanguageProvider, islCoreFoldersProvider, serverConnectionsProvider, - islCoreEmbeddedPluginJarLocator, + slCoreEmbeddedPluginProvider, compatibleNodeLocator, activeSolutionBoundTracker, configScopeUpdater, diff --git a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs index f3461e61f6..77b28e69ba 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceHandleTests.cs @@ -56,13 +56,14 @@ public class SLCoreInstanceHandleTests private static readonly List JarList = new() { "jar1" }; private static readonly Dictionary ConnectedModeJarList = new() { { "key", "jar1" } }; + private static readonly List DisabledAnalysisPluginKeys = [Language.CS.GetPluginKey()]; private ISLCoreRpcFactory slCoreRpcFactory; private ISLCoreRpcManager rpcManager; private ISLCoreConstantsProvider constantsProvider; private ISLCoreLanguageProvider slCoreLanguageProvider; private ISLCoreFoldersProvider foldersProvider; private IServerConnectionsProvider connectionsProvider; - private ISLCoreEmbeddedPluginJarLocator jarLocator; + private ISLCoreEmbeddedPluginProvider jarProvider; private INodeLocationProvider nodeLocator; private IEsLintBridgeLocator esLintBridgeLocator; private IActiveSolutionBoundTracker activeSolutionBoundTracker; @@ -81,7 +82,7 @@ public void TestInitialize() slCoreLanguageProvider = Substitute.For(); foldersProvider = Substitute.For(); connectionsProvider = Substitute.For(); - jarLocator = Substitute.For(); + jarProvider = Substitute.For(); nodeLocator = Substitute.For(); esLintBridgeLocator = Substitute.For(); activeSolutionBoundTracker = Substitute.For(); @@ -97,7 +98,7 @@ public void TestInitialize() slCoreLanguageProvider, foldersProvider, connectionsProvider, - jarLocator, + jarProvider, nodeLocator, esLintBridgeLocator, activeSolutionBoundTracker, @@ -125,7 +126,7 @@ public void Initialize_RpcManagerThrows_DoesNotCatch() [DataRow(null, null)] public void Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, string esLintBridgePath) { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out _); nodeLocator.Get().Returns(nodeJsPath); esLintBridgeLocator.Get().Returns(esLintBridgePath); @@ -145,6 +146,7 @@ public void Initialize_SuccessfullyInitializesInCorrectOrder(string nodeJsPath, && parameters.workDir == WorkDir && parameters.embeddedPluginPaths == JarList && parameters.connectedModeEmbeddedPluginPathsByKey.Count == ConnectedModeJarList.Count + && parameters.disabledPluginKeysForAnalysis.SequenceEqual(DisabledAnalysisPluginKeys) && parameters.sonarQubeConnections.SequenceEqual(new[] { SonarQubeConnection1, SonarQubeConnection2 }) && parameters.sonarCloudConnections.SequenceEqual(new[] { SonarCloudConnection }) && parameters.sonarlintUserHome == UserHome @@ -165,8 +167,7 @@ public void Initialize_UsesProvidedLanguageConfiguration() { List standalone = [Language.CS, Language.HTML]; List connected = [Language.VBNET, Language.TSQL]; - List disabledAnalysis = [Language.CPP, Language.JS]; - SetUpLanguages(standalone, connected, disabledAnalysis); + SetUpLanguages(standalone, connected); SetUpFullConfiguration(out _); testSubject.Initialize(); @@ -174,7 +175,6 @@ public void Initialize_UsesProvidedLanguageConfiguration() var initializeParams = (InitializeParams)rpcManager.ReceivedCalls().Single().GetArguments().Single()!; initializeParams.enabledLanguagesInStandaloneMode.Should().BeSameAs(standalone); initializeParams.extraEnabledLanguagesInConnectedMode.Should().BeSameAs(connected); - initializeParams.disabledPluginKeysForAnalysis.Should().BeEquivalentTo(disabledAnalysis.Select(l => l.GetPluginKey())); } [TestMethod] @@ -191,7 +191,7 @@ public void Initialize_ProvidesRulesSettings() [TestMethod] public void Dispose_Initialized_ShutsDownAndDisposesRpc() { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); testSubject.Initialize(); @@ -211,7 +211,7 @@ public void Dispose_Initialized_ShutsDownAndDisposesRpc() [TestMethod] public void Dispose_IgnoresShutdownException() { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); rpcManager.When(x => x.Shutdown()).Do(_ => throw new Exception()); @@ -226,7 +226,7 @@ public void Dispose_IgnoresShutdownException() [TestMethod] public void Dispose_ConnectionDied_DisposesRpc() { - SetUpLanguages([], [], []); + SetUpLanguages([], []); SetUpFullConfiguration(out var rpc); testSubject.Initialize(); @@ -264,20 +264,19 @@ private void SetUpFullConfiguration(out ISLCoreRpc rpc) { { SonarQubeConnection1.connectionId, SonarQubeConnection1 }, { SonarQubeConnection2.connectionId, SonarQubeConnection2 }, { SonarCloudConnection.connectionId, SonarCloudConnection } }); - jarLocator.ListJarFiles().Returns(JarList); - jarLocator.ListConnectedModeEmbeddedPluginPathsByKey().Returns(ConnectedModeJarList); + jarProvider.ListJarFiles().Returns(JarList); + jarProvider.ListConnectedModeEmbeddedPluginPathsByKey().Returns(ConnectedModeJarList); + jarProvider.ListDisabledPluginKeysForAnalysis().Returns(DisabledAnalysisPluginKeys); activeSolutionBoundTracker.CurrentConfiguration.Returns(new BindingConfiguration(Binding, SonarLintMode.Connected, "dir")); slCoreRuleSettingsProvider.GetSLCoreRuleSettings().Returns(new Dictionary()); } private void SetUpLanguages( List standalone, - List connected, - List disabledAnalysis) + List connected) { slCoreLanguageProvider.LanguagesInStandaloneMode.Returns(standalone); slCoreLanguageProvider.ExtraLanguagesInConnectedMode.Returns(connected); - slCoreLanguageProvider.LanguagesWithDisabledAnalysis.Returns(disabledAnalysis); } #region RpcSetUp diff --git a/src/SLCore/Configuration/ISLCoreEmbeddedPluginJarLocator.cs b/src/SLCore/Configuration/ISLCoreEmbeddedPluginProvider.cs similarity index 91% rename from src/SLCore/Configuration/ISLCoreEmbeddedPluginJarLocator.cs rename to src/SLCore/Configuration/ISLCoreEmbeddedPluginProvider.cs index ef47f4b4ee..368bf643dd 100644 --- a/src/SLCore/Configuration/ISLCoreEmbeddedPluginJarLocator.cs +++ b/src/SLCore/Configuration/ISLCoreEmbeddedPluginProvider.cs @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; - namespace SonarLint.VisualStudio.SLCore.Configuration; -public interface ISLCoreEmbeddedPluginJarLocator +public interface ISLCoreEmbeddedPluginProvider { List ListJarFiles(); Dictionary ListConnectedModeEmbeddedPluginPathsByKey(); + + List ListDisabledPluginKeysForAnalysis(); } diff --git a/src/SLCore/Configuration/ISLCoreLanguageProvider.cs b/src/SLCore/Configuration/ISLCoreLanguageProvider.cs index 4f17e413e6..9f77acf53e 100644 --- a/src/SLCore/Configuration/ISLCoreLanguageProvider.cs +++ b/src/SLCore/Configuration/ISLCoreLanguageProvider.cs @@ -29,8 +29,6 @@ public interface ISLCoreLanguageProvider { IReadOnlyList LanguagesInStandaloneMode { get; } IReadOnlyList ExtraLanguagesInConnectedMode { get; } - IReadOnlyList AllAnalyzableLanguages { get; } - IReadOnlyList LanguagesWithDisabledAnalysis { get; } } [Export(typeof(ISLCoreLanguageProvider))] @@ -42,12 +40,8 @@ public SLCoreLanguageProvider(ILanguageProvider languageProvider) { LanguagesInStandaloneMode = languageProvider.LanguagesInStandaloneMode.Select(x => x.ConvertToSlCoreLanguage()).ToList(); ExtraLanguagesInConnectedMode = languageProvider.ExtraLanguagesInConnectedMode.Select(x => x.ConvertToSlCoreLanguage()).ToList(); - LanguagesWithDisabledAnalysis = languageProvider.RoslynLanguages.Select(x => x.ConvertToSlCoreLanguage()).ToList(); - AllAnalyzableLanguages = LanguagesInStandaloneMode.Concat(ExtraLanguagesInConnectedMode).Except(LanguagesWithDisabledAnalysis).ToList(); } public IReadOnlyList LanguagesInStandaloneMode { get; } public IReadOnlyList ExtraLanguagesInConnectedMode { get; } - public IReadOnlyList LanguagesWithDisabledAnalysis { get; } - public IReadOnlyList AllAnalyzableLanguages { get; } } diff --git a/src/SLCore/ISLCoreInstanceFactory.cs b/src/SLCore/ISLCoreInstanceFactory.cs index 5d5fc3afcc..7e79441a4e 100644 --- a/src/SLCore/ISLCoreInstanceFactory.cs +++ b/src/SLCore/ISLCoreInstanceFactory.cs @@ -46,7 +46,7 @@ internal class SLCoreInstanceFactory : ISLCoreInstanceFactory private readonly ISLCoreLanguageProvider slCoreLanguageProvider; private readonly ISLCoreFoldersProvider slCoreFoldersProvider; private readonly IServerConnectionsProvider serverConnectionConfigurationProvider; - private readonly ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider; + private readonly ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginProvider; private readonly INodeLocationProvider nodeLocator; private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; private readonly IConfigScopeUpdater configScopeUpdater; @@ -63,7 +63,7 @@ public SLCoreInstanceFactory( ISLCoreLanguageProvider slCoreLanguageProvider, ISLCoreFoldersProvider slCoreFoldersProvider, IServerConnectionsProvider serverConnectionConfigurationProvider, - ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider, + ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginProvider, INodeLocationProvider nodeLocator, IActiveSolutionBoundTracker activeSolutionBoundTracker, IConfigScopeUpdater configScopeUpdater, @@ -78,7 +78,7 @@ public SLCoreInstanceFactory( this.slCoreLanguageProvider = slCoreLanguageProvider; this.slCoreFoldersProvider = slCoreFoldersProvider; this.serverConnectionConfigurationProvider = serverConnectionConfigurationProvider; - this.slCoreEmbeddedPluginJarProvider = slCoreEmbeddedPluginJarProvider; + this.slCoreEmbeddedPluginProvider = slCoreEmbeddedPluginProvider; this.nodeLocator = nodeLocator; this.activeSolutionBoundTracker = activeSolutionBoundTracker; this.configScopeUpdater = configScopeUpdater; @@ -96,7 +96,7 @@ public ISLCoreInstanceHandle CreateInstance() => slCoreLanguageProvider, slCoreFoldersProvider, serverConnectionConfigurationProvider, - slCoreEmbeddedPluginJarProvider, + slCoreEmbeddedPluginProvider, nodeLocator, esLintBridgeLocator, activeSolutionBoundTracker, diff --git a/src/SLCore/ISLCoreInstanceHandle.cs b/src/SLCore/ISLCoreInstanceHandle.cs index fa65d5a87e..b797ad32ed 100644 --- a/src/SLCore/ISLCoreInstanceHandle.cs +++ b/src/SLCore/ISLCoreInstanceHandle.cs @@ -23,7 +23,6 @@ using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.SLCore.Analysis; -using SonarLint.VisualStudio.SLCore.Common.Helpers; using SonarLint.VisualStudio.SLCore.Configuration; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.EsLintBridge; @@ -52,7 +51,7 @@ internal sealed class SLCoreInstanceHandle : ISLCoreInstanceHandle private readonly ISLCoreConstantsProvider constantsProvider; private readonly ISLCoreLanguageProvider slCoreLanguageProvider; private readonly ISLCoreFoldersProvider slCoreFoldersProvider; - private readonly ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider; + private readonly ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginJarProvider; private readonly ISLCoreRuleSettingsProvider slCoreRuleSettingsProvider; private readonly ISlCoreTelemetryMigrationProvider telemetryMigrationProvider; private readonly IEsLintBridgeLocator esLintBridgeLocator; @@ -68,7 +67,7 @@ internal SLCoreInstanceHandle( ISLCoreLanguageProvider slCoreLanguageProvider, ISLCoreFoldersProvider slCoreFoldersProvider, IServerConnectionsProvider serverConnectionConfigurationProvider, - ISLCoreEmbeddedPluginJarLocator slCoreEmbeddedPluginJarProvider, + ISLCoreEmbeddedPluginProvider slCoreEmbeddedPluginJarProvider, INodeLocationProvider nodeLocator, IEsLintBridgeLocator esLintBridgeLocator, IActiveSolutionBoundTracker activeSolutionBoundTracker, @@ -99,8 +98,6 @@ public void Initialize() SLCoreRpc = slCoreRpcFactory.StartNewRpcInstance(); - - var serverConnectionConfigurations = serverConnectionConfigurationProvider.GetServerConnections(); var (storageRoot, workDir, sonarlintUserHome) = slCoreFoldersProvider.GetWorkFolders(); @@ -114,7 +111,7 @@ public void Initialize() connectedModeEmbeddedPluginPathsByKey: slCoreEmbeddedPluginJarProvider.ListConnectedModeEmbeddedPluginPathsByKey(), enabledLanguagesInStandaloneMode: slCoreLanguageProvider.LanguagesInStandaloneMode, extraEnabledLanguagesInConnectedMode: slCoreLanguageProvider.ExtraLanguagesInConnectedMode, - disabledPluginKeysForAnalysis: slCoreLanguageProvider.LanguagesWithDisabledAnalysis.Select(l => l.GetPluginKey()).ToList(), + disabledPluginKeysForAnalysis: slCoreEmbeddedPluginJarProvider.ListDisabledPluginKeysForAnalysis(), serverConnectionConfigurations.Values.OfType().ToList(), serverConnectionConfigurations.Values.OfType().ToList(), sonarlintUserHome, From 2957580b40f9b5f1cf9c74729473aaedeb09c2fb Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:26:46 +0200 Subject: [PATCH 13/38] SLVS-2482 Add analyzer provider & loader (#6376) [SLVS-2482](https://sonarsource.atlassian.net/browse/SLVS-2482) Part of SLVS-2406 [SLVS-2482]: https://sonarsource.atlassian.net/browse/SLVS-2482?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../RoslynBindingConfigProviderTests.cs | 5 +- .../RoslynQualityProfileDownloaderTests.cs | 29 ++-- .../Binding/RoslynBindingConfigProvider.cs | 4 +- src/Core.UnitTests/LanguageTests.cs | 10 +- src/Core/CSharpVB/IRoslynConfigGenerator.cs | 2 +- src/Core/ILanguageProvider.cs | 4 +- src/Core/Language.cs | 50 +++++-- .../EmbeddedDotnetAnalyzerProviderTests.cs | 9 +- .../Roslyn/EmbeddedDotnetAnalyzerProvider.cs | 3 +- .../Roslyn/IObsoleteDotnetAnalyzersLocator.cs | 27 ++++ .../CSharpVB/SonarLintConfigGeneratorTests.cs | 2 +- .../StandaloneRoslynSettingsUpdaterTests.cs | 25 ++-- .../EmbeddedDotnetAnalyzersLocatorTests.cs | 89 +++++++++++- .../EmbeddedDotnetAnalyzersLocator.cs | 47 ++++-- .../CSharpVB/RoslynConfigGenerator.cs | 2 +- ...oslynAnalysisConfigurationProviderTests.cs | 18 +-- .../RoslynAnalysisProfilesProviderTests.cs | 8 +- .../RoslynAnalyzerLoaderTests.cs | 46 ++++++ .../RoslynAnalyzerProviderTests.cs | 136 ++++++++++++++++++ .../IEmbeddedRoslynAnalyzersLocator.cs | 8 +- .../IRoslynAnalysisProfilesProvider.cs | 4 +- .../Configuration/IRoslynAnalyzerLoader.cs | 28 ++++ .../Configuration/IRoslynAnalyzerProvider.cs | 4 +- .../RoslynAnalysisConfigurationProvider.cs | 2 +- .../RoslynAnalysisProfilesProvider.cs | 12 +- .../Configuration/RoslynAnalyzerLoader.cs | 53 +++++++ .../Configuration/RoslynAnalyzerProvider.cs | 58 ++++++++ .../Resources.Designer.cs | 19 ++- src/RoslynAnalyzerServer/Resources.resx | 6 + .../SlCoreLanguageProviderTests.cs | 2 +- ...eService_GetSuppressedRoslynIssuesAsync.cs | 8 +- .../SonarQubeService_TestBase.cs | 2 +- .../Helpers/FakeRoslynLanguage.cs | 28 ++++ 33 files changed, 636 insertions(+), 114 deletions(-) create mode 100644 src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs rename src/{Infrastructure.VS/Roslyn => RoslynAnalyzerServer/Analysis/Configuration}/IEmbeddedRoslynAnalyzersLocator.cs (76%) create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs create mode 100644 src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs diff --git a/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs index e3ca19e64f..e2bcd45ff8 100644 --- a/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client; using SonarQube.Client.Models; @@ -78,7 +79,7 @@ public void GetRules_UnsupportedLanguage_Throws() var testSubject = builder.CreateTestSubject(); // Act - Action act = () => testSubject.SaveConfigurationAsync(validQualityProfile, Language.Cpp, BindingConfiguration.Standalone, CancellationToken.None).Wait(); + Action act = () => testSubject.SaveConfigurationAsync(validQualityProfile, FakeRoslynLanguage.Instance, BindingConfiguration.Standalone, CancellationToken.None).Wait(); // Assert act.Should().ThrowExactly().And.ParamName.Should().Be("language"); @@ -122,7 +123,7 @@ public async Task GetConfig_NoSupportedActiveRules_Throws() var expectedOutput = string.Format(BindingStrings.SubTextPaddingFormat, string.Format(BindingStrings.NoSonarAnalyzerActiveRulesForQualityProfile, validQualityProfile.Name, Language.VBNET.Name)); builder.Logger.AssertOutputStrings(expectedOutput); - builder.RoslynConfigGenerator.DidNotReceiveWithAnyArgs().GenerateAndSaveConfiguration(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any>(), Arg.Any>()); + builder.RoslynConfigGenerator.DidNotReceiveWithAnyArgs().GenerateAndSaveConfiguration(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any>(), Arg.Any>()); } [TestMethod] diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs index ea3ba021d8..f22b9107ba 100644 --- a/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs +++ b/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs @@ -22,6 +22,7 @@ using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; using SonarLint.VisualStudio.TestInfrastructure; using SonarQube.Client.Models; @@ -154,12 +155,12 @@ public async Task UpdateAsync_UpdatesOnlyRoslynLanguages() // Configure available languages on the server SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, boundProject, - Language.CSharp, Language.VBNET, Language.Cpp); + Language.CSharp, Language.VBNET, FakeRoslynLanguage.Instance); var configProvider = new Mock(MockBehavior.Strict); SetupConfigSave(configProvider, Language.CSharp); SetupConfigSave(configProvider, Language.VBNET); - SetupConfigSave(configProvider, Language.Cpp); + SetupConfigSave(configProvider, FakeRoslynLanguage.Instance); var configPersister = new DummyConfigPersister(); @@ -178,7 +179,7 @@ public async Task UpdateAsync_UpdatesOnlyRoslynLanguages() CheckRuleConfigSaved(configProvider, Language.CSharp); CheckRuleConfigSaved(configProvider, Language.VBNET); - CheckRuleConfigNotSaved(configProvider, Language.Cpp); + CheckRuleConfigNotSaved(configProvider, FakeRoslynLanguage.Instance); boundProject.Profiles.Count.Should().Be(2); boundProject.Profiles[Language.VBNET].ProfileKey.Should().NotBeNull(); @@ -195,9 +196,8 @@ public async Task UpdateAsync_WhenQualityProfileIsNotAvailable_OtherLanguagesDow var logger = new TestLogger(logToConsole: true); var languagesToBind = new[] { - Language.Cpp, // unavailable + FakeRoslynLanguage.Instance, // unavailable Language.CSharp, - Language.Secrets, // unavailable Language.VBNET }; @@ -208,9 +208,8 @@ public async Task UpdateAsync_WhenQualityProfileIsNotAvailable_OtherLanguagesDow Language.VBNET); var configProvider = new Mock(MockBehavior.Strict); - SetupConfigSave(configProvider, Language.Cpp); + SetupConfigSave(configProvider, FakeRoslynLanguage.Instance); SetupConfigSave(configProvider, Language.CSharp); - SetupConfigSave(configProvider, Language.Secrets); SetupConfigSave(configProvider, Language.VBNET); var configPersister = new DummyConfigPersister(); @@ -230,14 +229,12 @@ public async Task UpdateAsync_WhenQualityProfileIsNotAvailable_OtherLanguagesDow CheckRuleConfigSaved(configProvider, Language.CSharp); CheckRuleConfigSaved(configProvider, Language.VBNET); - CheckRuleConfigNotSaved(configProvider, Language.Cpp); - CheckRuleConfigNotSaved(configProvider, Language.Secrets); + CheckRuleConfigNotSaved(configProvider, FakeRoslynLanguage.Instance); - boundProject.Profiles.Count.Should().Be(4); + boundProject.Profiles.Count.Should().Be(3); boundProject.Profiles[Language.VBNET].ProfileKey.Should().NotBeNull(); boundProject.Profiles[Language.CSharp].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[Language.Cpp].ProfileKey.Should().BeNull(); - boundProject.Profiles[Language.Secrets].ProfileKey.Should().BeNull(); + boundProject.Profiles[FakeRoslynLanguage.Instance].ProfileKey.Should().BeNull(); } [TestMethod] @@ -320,7 +317,7 @@ private static void SetupLanguagesToUpdate( private static void SetupConfigSave( Mock bindingConfigProvider, - Language language) + RoslynLanguage language) { bindingConfigProvider.Setup(x => x.SaveConfigurationAsync( It.IsAny(), @@ -338,7 +335,7 @@ private static BoundServerProject CreateBoundProject( projectKey, new ServerConnection.SonarQube(uri ?? new Uri("http://localhost/"))); - private static void CheckRuleConfigSaved(Mock bindingConfig, Language language) => + private static void CheckRuleConfigSaved(Mock bindingConfig, RoslynLanguage language) => bindingConfig.Verify( x => x.SaveConfigurationAsync( @@ -348,7 +345,7 @@ private static void CheckRuleConfigSaved(Mock bindingCon It.IsAny()), Times.Once); - private static void CheckRuleConfigNotSaved(Mock bindingConfig, Language language) => + private static void CheckRuleConfigNotSaved(Mock bindingConfig, RoslynLanguage language) => bindingConfig.Verify( x => x.SaveConfigurationAsync( @@ -369,7 +366,7 @@ BindingConfiguration IConfigurationPersister.Persist(BoundServerProject project) } } - private static Mock CreateLanguageProvider(Language[] languagesToBind = null) + private static Mock CreateLanguageProvider(RoslynLanguage[] languagesToBind = null) { var mockLanguageProvider = new Mock(); mockLanguageProvider.Setup(x => x.RoslynLanguages) diff --git a/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs b/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs index 7a809b8058..98b8702a68 100644 --- a/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs +++ b/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs @@ -58,12 +58,12 @@ public Task SaveConfigurationAsync( throw new ArgumentOutOfRangeException(nameof(language)); } - return SaveConfigurationInternalAsync(qualityProfile, language, bindingConfiguration, cancellationToken); + return SaveConfigurationInternalAsync(qualityProfile, language as RoslynLanguage, bindingConfiguration, cancellationToken); } private async Task SaveConfigurationInternalAsync( SonarQubeQualityProfile qualityProfile, - Language language, + RoslynLanguage language, BindingConfiguration bindingConfiguration, CancellationToken cancellationToken) { diff --git a/src/Core.UnitTests/LanguageTests.cs b/src/Core.UnitTests/LanguageTests.cs index d43177ce07..cd30b5f6de 100644 --- a/src/Core.UnitTests/LanguageTests.cs +++ b/src/Core.UnitTests/LanguageTests.cs @@ -37,22 +37,22 @@ public void Language_Ctor_ArgChecks() // Act + Assert // Nulls - Action act = () => new Language(name, null, serverLanguageKey, pluginInfo, repoInfo, settingsFileName: fileSuffix); + Action act = () => new Language(name, null, serverLanguageKey, pluginInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("name"); - act = () => new Language(null, key, serverLanguageKey, pluginInfo, repoInfo, settingsFileName: fileSuffix); + act = () => new Language(null, key, serverLanguageKey, pluginInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("id"); - act = () => new Language(name, key, null, pluginInfo, repoInfo, settingsFileName: fileSuffix); + act = () => new Language(name, key, null, pluginInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("serverLanguageKey"); act = () => new Language(name, key, serverLanguageKey, null, repoInfo, repoInfo); act.Should().ThrowExactly().And.ParamName.Should().Be("pluginInfo"); - act = () => new Language(name, key, serverLanguageKey, pluginInfo, null, settingsFileName: fileSuffix); + act = () => new Language(name, key, serverLanguageKey, pluginInfo, null); act.Should().ThrowExactly().And.ParamName.Should().Be("repoInfo"); - act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo: null, settingsFileName: fileSuffix); + act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo: null); act.Should().NotThrow(); act = () => new Language(name, key, serverLanguageKey, pluginInfo, repoInfo, additionalPlugins: null); diff --git a/src/Core/CSharpVB/IRoslynConfigGenerator.cs b/src/Core/CSharpVB/IRoslynConfigGenerator.cs index c40066f926..f357d4ebd4 100644 --- a/src/Core/CSharpVB/IRoslynConfigGenerator.cs +++ b/src/Core/CSharpVB/IRoslynConfigGenerator.cs @@ -23,7 +23,7 @@ namespace SonarLint.VisualStudio.Core.CSharpVB; public interface IRoslynConfigGenerator { void GenerateAndSaveConfiguration( - Language language, + RoslynLanguage language, string baseDirectory, IDictionary properties, IFileExclusions fileExclusions, diff --git a/src/Core/ILanguageProvider.cs b/src/Core/ILanguageProvider.cs index f0a7e790ef..b6d69a9369 100644 --- a/src/Core/ILanguageProvider.cs +++ b/src/Core/ILanguageProvider.cs @@ -28,7 +28,7 @@ public interface ILanguageProvider IReadOnlyList NonRoslynLanguages { get; } - IReadOnlyList RoslynLanguages { get; } + IReadOnlyList RoslynLanguages { get; } IReadOnlyList LanguagesInStandaloneMode { get; } @@ -56,7 +56,7 @@ public LanguageProvider() } public IReadOnlyList NonRoslynLanguages { get; } = [Language.C, Language.Cpp, Language.Js, Language.Ts, Language.Css, Language.Secrets, Language.Html, Language.TSql, Language.Text]; - public IReadOnlyList RoslynLanguages { get; } = [Language.CSharp, Language.VBNET]; + public IReadOnlyList RoslynLanguages { get; } = [Language.CSharp, Language.VBNET]; public IReadOnlyList AllKnownLanguages { get; } public IReadOnlyList LanguagesInStandaloneMode { get; } public IReadOnlyList ExtraLanguagesInConnectedMode { get; } = [Language.TSql, Language.Text]; diff --git a/src/Core/Language.cs b/src/Core/Language.cs index e5a6218529..95c19617f2 100644 --- a/src/Core/Language.cs +++ b/src/Core/Language.cs @@ -35,7 +35,7 @@ namespace SonarLint.VisualStudio.Core /// [DebuggerDisplay("{Name} (ID: {Id})")] [TypeConverter(typeof(LanguageConverter))] - public sealed class Language : IEquatable + public class Language : IEquatable { private const string VersionNumberPattern = "(\\d+\\.\\d+\\.\\d+\\.\\d+)\\"; private static readonly PluginInfo SqvsRoslynPlugin = new("sqvsroslyn", $"sonarqube-ide-visualstudio-roslyn-plugin-{VersionNumberPattern}.jar"); @@ -63,9 +63,9 @@ public sealed class Language : IEquatable private static readonly RepoInfo TsqlRepo = new("tsql"); public static readonly Language Unknown = new(); - public static readonly Language CSharp = new("CSharp", CoreStrings.CSharpLanguageName, "cs", SqvsRoslynPlugin, CSharpRepo, CSharpSecurityRepo, - settingsFileName: "sonarlint_csharp.globalconfig", additionalPlugins: [CSharpPlugin]); - public static readonly Language VBNET = new("VB", CoreStrings.VBNetLanguageName, "vbnet", SqvsRoslynPlugin, VbNetRepo, settingsFileName: "sonarlint_vb.globalconfig", + public static readonly RoslynLanguage CSharp = new("CSharp", CoreStrings.CSharpLanguageName, "cs", SqvsRoslynPlugin, CSharpRepo, + settingsFileName: "sonarlint_csharp.globalconfig", roslynDllIdentifier: ".CSharp.", CSharpSecurityRepo, additionalPlugins: [CSharpPlugin]); + public static readonly RoslynLanguage VBNET = new("VB", CoreStrings.VBNetLanguageName, "vbnet", SqvsRoslynPlugin, VbNetRepo, settingsFileName: "sonarlint_vb.globalconfig", roslynDllIdentifier: ".VisualBasic.", additionalPlugins: [VbNetPlugin]); public static readonly Language Cpp = new("C++", CoreStrings.CppLanguageName, "cpp", CFamilyPlugin, CppRepo); public static readonly Language C = new("C", "C", "c", CFamilyPlugin, CRepo); @@ -97,12 +97,6 @@ public sealed class Language : IEquatable /// public string Name { get; } - /// - /// Suffix and extension added to the language-specific rules configuration file for the language - /// - /// e.g. for ruleset-based languages this will be a language identifier + ".globalconfig" - public string SettingsFileNameAndExtension { get; } - /// /// Additional plugins that should be installed for a language /// @@ -122,7 +116,6 @@ private Language() { Id = string.Empty; Name = CoreStrings.UnknownLanguageName; - SettingsFileNameAndExtension = string.Empty; } public Language( @@ -132,7 +125,6 @@ public Language( PluginInfo pluginInfo, RepoInfo repoInfo, RepoInfo securityRepoInfo = null, - string settingsFileName = null, PluginInfo[] additionalPlugins = null) { if (string.IsNullOrWhiteSpace(id)) @@ -147,7 +139,6 @@ public Language( Id = id; Name = name; - SettingsFileNameAndExtension = settingsFileName; AdditionalPlugins = additionalPlugins; ServerLanguageKey = serverLanguageKey ?? throw new ArgumentNullException(nameof(serverLanguageKey)); PluginInfo = pluginInfo ?? throw new ArgumentNullException(nameof(pluginInfo)); @@ -184,4 +175,37 @@ public override int GetHashCode() public bool HasRepoKey(string repoKey) => RepoInfo.Key == repoKey || SecurityRepoInfo?.Key == repoKey; } + + /// + /// Represents a Roslyn-based programming language with specific Roslyn analyzer configuration. + /// + public class RoslynLanguage : Language + { + /// + /// Suffix and extension added to the language-specific rules configuration file for the language + /// + /// e.g. for ruleset-based languages this will be a language identifier + ".globalconfig" + public string SettingsFileNameAndExtension { get; } + + /// + /// A substring that is contained in the name of the analyzer files for specific roslyn language. + /// + public string RoslynDllIdentifier { get; } + + public RoslynLanguage( + string id, + string name, + string serverLanguageKey, + PluginInfo pluginInfo, + RepoInfo repoInfo, + string settingsFileName, + string roslynDllIdentifier, + RepoInfo securityRepoInfo = null, + PluginInfo[] additionalPlugins = null) + : base(id, name, serverLanguageKey, pluginInfo, repoInfo, securityRepoInfo, additionalPlugins) + { + SettingsFileNameAndExtension = settingsFileName; + RoslynDllIdentifier = roslynDllIdentifier; + } + } } diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs index e85d35f77e..e9e58de46b 100644 --- a/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs +++ b/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs @@ -24,6 +24,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; @@ -34,7 +35,7 @@ public class EmbeddedDotnetAnalyzerProviderTests private const string AnalyzersPath = "C:\\somepath"; private readonly IAnalyzerAssemblyLoader analyzerAssemblyLoader = Substitute.For(); private EmbeddedDotnetAnalyzerProvider testSubject; - private IEmbeddedDotnetAnalyzersLocator locator; + private IObsoleteDotnetAnalyzersLocator locator; private IAnalyzerAssemblyLoaderFactory loaderFactory; private IConfigurationScopeDotnetAnalyzerIndicator indicator; private ILogger logger; @@ -43,7 +44,7 @@ public class EmbeddedDotnetAnalyzerProviderTests [TestInitialize] public void TestInitialize() { - locator = Substitute.For(); + locator = Substitute.For(); loaderFactory = Substitute.For(); loaderFactory.Create().Returns(analyzerAssemblyLoader); logger = Substitute.For(); @@ -58,13 +59,13 @@ public void TestInitialize() public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), diff --git a/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs b/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs index 69b0fc516d..e61f09c79a 100644 --- a/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs +++ b/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs @@ -24,6 +24,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Core.CSharpVB; namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; @@ -32,7 +33,7 @@ namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] public class EmbeddedDotnetAnalyzerProvider( - IEmbeddedDotnetAnalyzersLocator locator, + IObsoleteDotnetAnalyzersLocator locator, IAnalyzerAssemblyLoaderFactory analyzerAssemblyLoaderFactory, IConfigurationScopeDotnetAnalyzerIndicator indicator, ILogger logger, diff --git a/src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs b/src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs new file mode 100644 index 0000000000..69e1eb61cf --- /dev/null +++ b/src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs @@ -0,0 +1,27 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; + +public interface IObsoleteDotnetAnalyzersLocator +{ + List GetBasicAnalyzerFullPaths(); + List GetEnterpriseAnalyzerFullPaths(); +} diff --git a/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs index e95a56febb..4ddd7be439 100644 --- a/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs +++ b/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs @@ -70,7 +70,7 @@ public void Generate_NullArguments_Throws() public void Generate_UnrecognisedLanguage_Throws(string languageKey) { Action act = () => testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), - new Language(languageKey, "languageX", languageKey, new PluginInfo("pluginKey", null), new RepoInfo("repoKey"), settingsFileName: ".any")); + new Language(languageKey, "languageX", languageKey, new PluginInfo("pluginKey", null), new RepoInfo("repoKey"))); act.Should().ThrowExactly().And.ParamName.Should().Be("language"); } diff --git a/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs b/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs index b61da9d82b..e2e399fcb2 100644 --- a/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs +++ b/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.Core.UserRuleSettings; using SonarLint.VisualStudio.Integration.CSharpVB.StandaloneMode; +using SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB.StandaloneMode; @@ -58,13 +59,12 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport()); [TestMethod] - public void MefCtor_CheckIsSingleton() => - MefTestHelpers.CheckIsSingletonMefComponent(); + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] public void Update_CallsGeneratorWithCorrectLanguageAndDirectory() { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.TSql, Language.C]; + IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp, FakeRoslynLanguage.Instance]; languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); var userSettings = new UserSettings(new AnalysisSettings(), @"APPDATA\SonarLint for Visual Studio\.global"); @@ -88,16 +88,16 @@ public void Update_CallsGeneratorWithCorrectLanguageAndDirectory() [TestMethod] public void Update_CallsGeneratorWithCorrectProperties() { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.TSql, Language.C]; + IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp]; languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); var properties = ImmutableDictionary.Create().SetItem("key", "value"); - var userSettings = new UserSettings(new AnalysisSettings(analysisProperties: properties), "any"); + var userSettings = new UserSettings(new AnalysisSettings(analysisProperties: properties), "any"); testSubject.Update(userSettings); Received.InOrder(() => { - foreach (var language in fakeRoslynLanguages) + foreach (RoslynLanguage language in fakeRoslynLanguages) { roslynConfigGenerator.GenerateAndSaveConfiguration( language, @@ -113,7 +113,7 @@ public void Update_CallsGeneratorWithCorrectProperties() [TestMethod] public void Update_ConvertsExclusionsCorrectly() { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.TSql, Language.C]; + IReadOnlyList fakeRoslynLanguages = [Language.VBNET, FakeRoslynLanguage.Instance]; languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); testSubject.Update(new UserSettings(new AnalysisSettings([], ["one", "two"], []), "any")); @@ -136,7 +136,7 @@ public void Update_ConvertsExclusionsCorrectly() [TestMethod] public void Update_ConvertsRulesCorrectly() { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET]; + IReadOnlyList fakeRoslynLanguages = [Language.VBNET]; languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); var rules = new Dictionary() { @@ -178,11 +178,14 @@ public void Update_ConvertsRulesCorrectly() [TestMethod] public void Update_GroupsRulesByLanguage() { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp]; + IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp]; languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); var rules = new Dictionary() { - { "vbnet:S1", new RuleConfig(default) }, { "vbnet:S2", new RuleConfig(default) }, { "csharpsquid:S3", new RuleConfig(default) }, { "cpp:S4", new RuleConfig(default) }, + { "vbnet:S1", new RuleConfig(default) }, + { "vbnet:S2", new RuleConfig(default) }, + { "csharpsquid:S3", new RuleConfig(default) }, + { $"{FakeRoslynLanguage.Instance.RepoInfo.Key}:S4", new RuleConfig(default) }, }; testSubject.Update(new UserSettings(new AnalysisSettings(rules, [], []), "any")); @@ -209,7 +212,7 @@ public void Update_GroupsRulesByLanguage() roslynConfigGenerator .DidNotReceive() .GenerateAndSaveConfiguration( - Language.Cpp, + FakeRoslynLanguage.Instance, Arg.Any(), Arg.Any>(), Arg.Any(), diff --git a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs index 7db8c7a109..338dbb5e0c 100644 --- a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs @@ -19,10 +19,12 @@ */ using System.IO; -using System.IO.Abstractions; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; using SonarLint.VisualStudio.Integration.Vsix.Helpers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; namespace SonarLint.VisualStudio.Integration.UnitTests.EmbeddedAnalyzers; @@ -31,23 +33,33 @@ public class EmbeddedDotnetAnalyzersLocatorTests { private const string PathInsideVsix = "C:\\somePath"; + private static readonly string CSharpRegularAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.CSharp.dll"); + private static readonly string VbRegularAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.VisualBasic.dll"); + private static readonly string CSharpEnterpriseAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.CSharp.dll"); + private static readonly string VbEnterpriseAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.VisualBasic.dll"); + private EmbeddedDotnetAnalyzersLocator testSubject; private IVsixRootLocator vsixRootLocator; - private IFileSystem fileSystem; + private IFileSystemService fileSystem; + private ILanguageProvider languageProvider; [TestInitialize] public void TestInitialize() { vsixRootLocator = Substitute.For(); - fileSystem = Substitute.For(); - testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, fileSystem); + languageProvider = Substitute.For(); + languageProvider.RoslynLanguages.Returns([Language.CSharp, Language.VBNET]); + fileSystem = Substitute.For(); + testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, languageProvider, fileSystem); } [TestMethod] public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); } [TestMethod] @@ -136,6 +148,71 @@ public void GetAnalyzerFullPaths_SearchesForFilesInsideVsix() fileSystem.Directory.Received(1).GetFiles(Path.Combine(PathInsideVsix, "EmbeddedDotnetAnalyzerDLLs"), "SonarAnalyzer.*.dll"); } + [TestMethod] + public void GetBasicAnalyzerFullPathsByLanguage_GroupsDllsByLanguageAndFiltersEnterprise() + { + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ + CSharpRegularAnalyzer, + VbRegularAnalyzer, + CSharpEnterpriseAnalyzer + ]); + + testSubject.GetBasicAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> + { + [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [VbRegularAnalyzer] + }); + } + + [TestMethod] + public void GetBasicAnalyzerFullPathsByLanguage_IncludesAllLanguagesEvenWithNoAnalyzers() + { + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([CSharpRegularAnalyzer]); + + testSubject.GetBasicAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [] }); + } + + [TestMethod] + public void GetEnterpriseAnalyzerFullPathsByLanguage_GroupsDllsByLanguageIncludingEnterprise() + { + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ + CSharpRegularAnalyzer, + VbRegularAnalyzer, + CSharpEnterpriseAnalyzer, + VbEnterpriseAnalyzer + ]); + + testSubject.GetEnterpriseAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> + { + [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], [Language.VBNET] = [VbRegularAnalyzer, VbEnterpriseAnalyzer] + }); + } + + [TestMethod] + public void GetEnterpriseAnalyzerFullPathsByLanguage_IncludesAllLanguagesEvenWithNoAnalyzers() + { + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([VbEnterpriseAnalyzer]); + + testSubject.GetEnterpriseAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> { [Language.CSharp] = [], [Language.VBNET] = [VbEnterpriseAnalyzer] }); + } + + [TestMethod] + public void GetEnterpriseAnalyzerFullPathsByLanguage_ExcludesLanguagesNotInRoslynLanguages() + { + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ + CSharpRegularAnalyzer, + VbRegularAnalyzer, + CSharpEnterpriseAnalyzer, + VbEnterpriseAnalyzer + ]); + // Only C# is in the Roslyn languages, VB.NET is not + languageProvider.RoslynLanguages.Returns([Language.CSharp]); + + testSubject.GetEnterpriseAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> + { + [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer] + }); + } + private static string GetAnalyzerFullPath(string pathInsideVsix, string analyzerFile) { return Path.Combine(pathInsideVsix, analyzerFile); diff --git a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs index d309404ca1..9a4592e42e 100644 --- a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs +++ b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs @@ -21,38 +21,55 @@ using System.ComponentModel.Composition; using System.IO; using System.IO.Abstractions; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; using SonarLint.VisualStudio.Integration.Vsix.Helpers; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; [Export(typeof(IEmbeddedDotnetAnalyzersLocator))] +[Export(typeof(IObsoleteDotnetAnalyzersLocator))] [PartCreationPolicy(CreationPolicy.Shared)] -internal class EmbeddedDotnetAnalyzersLocator : IEmbeddedDotnetAnalyzersLocator +[method: ImportingConstructor] +internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, ILanguageProvider languageProvider, IFileSystemService fileSystem) : IEmbeddedDotnetAnalyzersLocator, IObsoleteDotnetAnalyzersLocator { private const string PathInsideVsix = "EmbeddedDotnetAnalyzerDLLs"; private const string DllsSearchPattern = "SonarAnalyzer.*.dll"; // starting from 10.0, the analyzer assemblies are merged and all of the dll names start with SonarAnalyzer private const string EnterpriseInfix = ".Enterprise."; // enterprise analyzer assemblies are included in the same folder and need to be filtered out - private readonly IFileSystem fileSystem; - private readonly IVsixRootLocator vsixRootLocator; + private readonly IFileSystem fileSystem = fileSystem; - [ImportingConstructor] - public EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator) : this(vsixRootLocator, new FileSystem()) - { - } + public List GetBasicAnalyzerFullPaths() => GetBasicAnalyzerDlls().ToList(); - internal EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, IFileSystem fileSystem) - { - this.vsixRootLocator = vsixRootLocator; - this.fileSystem = fileSystem; - } + public Dictionary> GetBasicAnalyzerFullPathsByLanguage() => GroupByLanguage(GetBasicAnalyzerDlls()); + + private IEnumerable GetBasicAnalyzerDlls() => GetAllAnalyzerDlls().Where(x => !x.Contains(EnterpriseInfix)); - public List GetBasicAnalyzerFullPaths() => GetAnalyzerDlls().Where(x => !x.Contains(EnterpriseInfix)).ToList(); + public List GetEnterpriseAnalyzerFullPaths() => GetAllAnalyzerDlls().ToList(); - public List GetEnterpriseAnalyzerFullPaths() => GetAnalyzerDlls().ToList(); + public Dictionary> GetEnterpriseAnalyzerFullPathsByLanguage() => GroupByLanguage(GetAllAnalyzerDlls()); - private string[] GetAnalyzerDlls() => fileSystem.Directory.GetFiles(GetPathToParentFolder(), DllsSearchPattern); + private string[] GetAllAnalyzerDlls() => fileSystem.Directory.GetFiles(GetPathToParentFolder(), DllsSearchPattern); private string GetPathToParentFolder() => Path.Combine(vsixRootLocator.GetVsixRoot(), PathInsideVsix); + + + private Dictionary> GroupByLanguage(IEnumerable analyzerDlls) + { + var languageToAnalyzerLocations = languageProvider.RoslynLanguages.ToDictionary(x => x, _ => new List()); + + foreach (var analyzerDll in analyzerDlls) + { + var dllLanguage = languageToAnalyzerLocations.Keys.FirstOrDefault(x => analyzerDll.Contains(x.RoslynDllIdentifier)); + if (dllLanguage != null) + { + languageToAnalyzerLocations[dllLanguage].Add(analyzerDll); + } + } + + return languageToAnalyzerLocations; + } } diff --git a/src/Integration/CSharpVB/RoslynConfigGenerator.cs b/src/Integration/CSharpVB/RoslynConfigGenerator.cs index d1caf0f2bc..e880cca159 100644 --- a/src/Integration/CSharpVB/RoslynConfigGenerator.cs +++ b/src/Integration/CSharpVB/RoslynConfigGenerator.cs @@ -43,7 +43,7 @@ internal class RoslynConfigGenerator( private const string SonarlintConfigFileName = "SonarLint.xml"; public void GenerateAndSaveConfiguration( - Language language, + RoslynLanguage language, string baseDirectory, IDictionary properties, IFileExclusions fileExclusions, diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs index c6741ef79d..6ef1173aba 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -30,8 +30,8 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configu [TestClass] public class RoslynAnalysisConfigurationProviderTests { - private static readonly ImmutableDictionary DefaultAnalyzers - = new Dictionary { { Language.CSharp, new AnalyzersAndSupportedRules() } }.ToImmutableDictionary(); + private static readonly ImmutableDictionary DefaultAnalyzers + = new Dictionary { { Language.CSharp, new AnalyzerAssemblyContents() } }.ToImmutableDictionary(); private static readonly List DefaultActiveRules = new(); private static readonly Dictionary DefaultAnalysisProperties = new(); @@ -46,7 +46,7 @@ public void TestInitialize() { sonarLintXmlProvider = Substitute.For(); roslynAnalyzerProvider = Substitute.For(); - roslynAnalyzerProvider.GetAnalyzersByLanguage().Returns(DefaultAnalyzers); + roslynAnalyzerProvider.LoadAnalyzerAssemblies().Returns(DefaultAnalyzers); analyzerProfilesProvider = Substitute.For(); testLogger = Substitute.ForPartsOf(); @@ -78,7 +78,7 @@ public void Ctor_SetsLogContext() => [TestMethod] public void GetConfiguration_CreatesConfigurationForEachLanguage() { - var roslynAnalysisProfiles = new Dictionary + var roslynAnalysisProfiles = new Dictionary { { Language.CSharp, new RoslynAnalysisProfile( @@ -114,7 +114,7 @@ public void GetConfiguration_CreatesConfigurationForEachLanguage() public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() { var language = Language.CSharp; - var roslynAnalysisProfiles = new Dictionary + var roslynAnalysisProfiles = new Dictionary { { language, new RoslynAnalysisProfile( @@ -137,7 +137,7 @@ public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() { var language = Language.CSharp; - var roslynAnalysisProfiles = new Dictionary + var roslynAnalysisProfiles = new Dictionary { { language, new RoslynAnalysisProfile( @@ -160,16 +160,16 @@ public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() public void GetConfiguration_NoAnalysisProfiles_ReturnsEmptyDictionary() { analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) - .Returns(new Dictionary()); + .Returns(new Dictionary()); var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); result.Should().BeEmpty(); } - private Dictionary SetUpXmlConfigurations(Dictionary profiles) + private Dictionary SetUpXmlConfigurations(Dictionary profiles) { - var xmlConfigurations = new Dictionary(); + var xmlConfigurations = new Dictionary(); foreach (var profile in profiles) { var xml = SetUpXmlProvider(profile.Value); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs index 159ca45890..369ecfdb1a 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs @@ -44,7 +44,7 @@ public class RoslynAnalysisProfilesProviderTests [TestMethod] public void GetAnalysisProfilesByLanguage_EmptyInputs_ReturnsEmptyDictionary() { - var supportedDiagnostics = ImmutableDictionary.Empty; + var supportedDiagnostics = ImmutableDictionary.Empty; var activeRules = new List(); Dictionary analysisProperties = []; @@ -105,9 +105,9 @@ private static RoslynRuleConfiguration CreateRuleConfiguration( isActive, parameters); - private static ImmutableDictionary CreateSupportedDiagnosticsForLanguages( - Dictionary analyzers) => + private static ImmutableDictionary CreateSupportedDiagnosticsForLanguages( + Dictionary analyzers) => analyzers.ToImmutableDictionary( x => x.Key, - y => new AnalyzersAndSupportedRules(y.Value.analyzers.ToImmutableArray(), y.Value.RuleKeys.ToImmutableArray())); + y => new AnalyzerAssemblyContents(y.Value.analyzers.ToImmutableArray(), y.Value.RuleKeys.ToImmutableHashSet())); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs new file mode 100644 index 0000000000..00249f7964 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs @@ -0,0 +1,46 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalyzerLoaderTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Ctor_SetsLogContext() + { + var logger = Substitute.For(); + + _ = new RoslynAnalyzerLoader(logger); + + logger.Received().ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs new file mode 100644 index 0000000000..1944fd9d32 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs @@ -0,0 +1,136 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class RoslynAnalyzerProviderTests +{ + private const string CsharpAnalyzerPath = "c:\\analyzers\\csharp.dll"; + private const string VbAnalyzerPath = "c:\\analyzers\\vb.dll"; + + private IEmbeddedDotnetAnalyzersLocator analyzersLocator = null!; + private IRoslynAnalyzerLoader roslynAnalyzerLoader = null!; + private RoslynAnalyzerProvider testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + analyzersLocator = Substitute.For(); + roslynAnalyzerLoader = Substitute.For(); + testSubject = new RoslynAnalyzerProvider(analyzersLocator, roslynAnalyzerLoader); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() + { + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary>()); + + var result = testSubject.LoadAnalyzerAssemblies(); + + result.Should().BeEmpty(); + } + + [TestMethod] + public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrectDictionary() + { + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("CS0001"); + var vbAnalyzer = CreateAnalyzerWithDiagnostic("VB0001"); + roslynAnalyzerLoader.LoadAnalyzers(CsharpAnalyzerPath).Returns([csharpAnalyzer]); + roslynAnalyzerLoader.LoadAnalyzers(VbAnalyzerPath).Returns([vbAnalyzer]); + + var result = testSubject.LoadAnalyzerAssemblies(); + + result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer); + result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("CS0001"); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(vbAnalyzer); + result[Language.VBNET].SupportedRuleKeys.Should().BeEquivalentTo("VB0001"); + } + + [TestMethod] + public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() + { + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); + var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001", "SDUPLICATE"); + var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "SDUPLICATE"); + var vbAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002"); + roslynAnalyzerLoader.LoadAnalyzers(CsharpAnalyzerPath).Returns([csharpAnalyzer1, csharpAnalyzer2]); + roslynAnalyzerLoader.LoadAnalyzers(VbAnalyzerPath).Returns([vbAnalyzer]); + + var result = testSubject.LoadAnalyzerAssemblies(); + + result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); + result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "SDUPLICATE", "S002"); + result[Language.VBNET].SupportedRuleKeys.Should().BeEquivalentTo("S001", "S002"); + } + + [TestMethod] + public void GetAnalyzersByLanguage_MultipleAnalyzersPerLanguage_CombinesAllRules() + { + const string csharpAnalyzerPath2 = "c:\\analyzers\\csharp2.dll"; + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath, csharpAnalyzerPath2] } }); + var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001"); + var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "S003"); + var csharpAnalyzer3 = CreateAnalyzerWithDiagnostic("S004"); + roslynAnalyzerLoader.LoadAnalyzers(CsharpAnalyzerPath).Returns([csharpAnalyzer1]); + roslynAnalyzerLoader.LoadAnalyzers(csharpAnalyzerPath2).Returns([csharpAnalyzer2, csharpAnalyzer3]); + + var result = testSubject.LoadAnalyzerAssemblies(); + + result.Keys.Should().BeEquivalentTo(Language.CSharp); + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer1, csharpAnalyzer2, csharpAnalyzer3); + result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "S002", "S003", "S004"); + } + + private static DiagnosticAnalyzer CreateAnalyzerWithDiagnostic(params string[] diagnosticIds) + { + var analyzer = Substitute.For(); + analyzer.SupportedDiagnostics.Returns(diagnosticIds.Select(CreateDiagnosticDescriptor).ToImmutableArray()); + return analyzer; + } + + private static DiagnosticDescriptor CreateDiagnosticDescriptor(string id) => + new( + id, + "any title", + "any message", + "any category", + DiagnosticSeverity.Warning, + true); +} diff --git a/src/Infrastructure.VS/Roslyn/IEmbeddedRoslynAnalyzersLocator.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs similarity index 76% rename from src/Infrastructure.VS/Roslyn/IEmbeddedRoslynAnalyzersLocator.cs rename to src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs index de06ed41c2..80fcef0fc9 100644 --- a/src/Infrastructure.VS/Roslyn/IEmbeddedRoslynAnalyzersLocator.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs @@ -18,11 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; public interface IEmbeddedDotnetAnalyzersLocator { - List GetBasicAnalyzerFullPaths(); + Dictionary> GetBasicAnalyzerFullPathsByLanguage(); - List GetEnterpriseAnalyzerFullPaths(); + Dictionary> GetEnterpriseAnalyzerFullPathsByLanguage(); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs index fc67ea0af3..4827641be4 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs @@ -27,8 +27,8 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalysisProfilesProvider { - Dictionary GetAnalysisProfilesByLanguage( - ImmutableDictionary supportedRulesByLanguage, + Dictionary GetAnalysisProfilesByLanguage( + ImmutableDictionary supportedRulesByLanguage, List activeRules, Dictionary? analysisProperties); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs new file mode 100644 index 0000000000..8293cac7b5 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal interface IRoslynAnalyzerLoader +{ + IReadOnlyCollection LoadAnalyzers(string filePath); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs index 0c6d63793c..3cf403e178 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs @@ -26,7 +26,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalyzerProvider { - ImmutableDictionary GetAnalyzersByLanguage(); + ImmutableDictionary LoadAnalyzerAssemblies(); } -internal record struct AnalyzersAndSupportedRules(ImmutableArray Analyzers, ImmutableArray SupportedRuleKeys); +internal record struct AnalyzerAssemblyContents(ImmutableArray Analyzers, ImmutableHashSet SupportedRuleKeys); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs index ec8889486b..83b279fb96 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -40,7 +40,7 @@ public IReadOnlyDictionary GetConfigurati { // todo add caching https://sonarsource.atlassian.net/browse/SLVS-2481 - var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.GetAnalyzersByLanguage(), activeRules, analysisProperties); + var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.LoadAnalyzerAssemblies(), activeRules, analysisProperties); var configurations = new Dictionary(); foreach (var analyzerAndLanguage in analysisProfilesByLanguage) diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs index 6479e99e36..3e145b53e6 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs @@ -29,8 +29,8 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [PartCreationPolicy(CreationPolicy.Shared)] internal class RoslynAnalysisProfilesProvider : IRoslynAnalysisProfilesProvider { - public Dictionary GetAnalysisProfilesByLanguage( - ImmutableDictionary supportedRulesByLanguage, + public Dictionary GetAnalysisProfilesByLanguage( + ImmutableDictionary supportedRulesByLanguage, List activeRules, Dictionary? analysisProperties) { @@ -41,7 +41,7 @@ public Dictionary GetAnalysisProfilesByLanguage return roslynAnalysisProfiles; } - private static Dictionary InitializeProfilesForEachLanguage(ImmutableDictionary supportedRulesByLanguage) + private static Dictionary InitializeProfilesForEachLanguage(ImmutableDictionary supportedRulesByLanguage) { var roslynAnalysisProfiles = supportedRulesByLanguage.ToDictionary(x => x.Key, x => new RoslynAnalysisProfile(x.Value.Analyzers, [], [])); return roslynAnalysisProfiles; @@ -49,8 +49,8 @@ private static Dictionary InitializeProfilesFor private static void AddRules( List activeRules, - ImmutableDictionary supportedRulesByLanguage, - Dictionary roslynAnalysisProfiles) + ImmutableDictionary supportedRulesByLanguage, + Dictionary roslynAnalysisProfiles) { var activeRulesById = activeRules.ToDictionary(x => x.RuleId, y => y); @@ -73,7 +73,7 @@ private static void AddRules( } } - private static void AddProperties(Dictionary? analysisProperties, Dictionary roslynAnalysisProfiles) + private static void AddProperties(Dictionary? analysisProperties, Dictionary roslynAnalysisProfiles) { if (analysisProperties == null) { diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs new file mode 100644 index 0000000000..6acb5a046a --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs @@ -0,0 +1,53 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalyzerLoader))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynAnalyzerLoader(ILogger logger) : IRoslynAnalyzerLoader +{ + private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); + + [ExcludeFromCodeCoverage] + public IReadOnlyCollection LoadAnalyzers(string filePath) + { + try + { + return Assembly.LoadFrom(filePath) + .GetTypes() + .Where(t => typeof(DiagnosticAnalyzer).IsAssignableFrom(t) && !t.IsAbstract) + .Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t)) + .ToList(); + } + catch (Exception e) + { + logger.WriteLine(Resources.RoslynAnalysisAnalyzerLoaderFailedToLoad, filePath, e); + return []; + } + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs new file mode 100644 index 0000000000..116d03bffc --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs @@ -0,0 +1,58 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Diagnostics; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.CSharpVB; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +[Export(typeof(IRoslynAnalyzerProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class RoslynAnalyzerProvider(IEmbeddedDotnetAnalyzersLocator analyzersLocator, IRoslynAnalyzerLoader roslynAnalyzerLoader) : IRoslynAnalyzerProvider +{ + public ImmutableDictionary LoadAnalyzerAssemblies() => + // todo SLVS-2410 Respect NET repackaging + LoadAnalyzersAndRules(analyzersLocator.GetBasicAnalyzerFullPathsByLanguage()); + + private ImmutableDictionary LoadAnalyzersAndRules(Dictionary> analyzerFullPathsByLanguage) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var languageAndAnalyzers in analyzerFullPathsByLanguage) + { + var supportedDiagnostics = ImmutableHashSet.CreateBuilder(); + var analyzers = ImmutableArray.CreateBuilder(); + + foreach (var diagnosticAnalyzer in languageAndAnalyzers.Value.SelectMany(roslynAnalyzerLoader.LoadAnalyzers)) + { + analyzers.Add(diagnosticAnalyzer); + supportedDiagnostics.UnionWith(diagnosticAnalyzer.SupportedDiagnostics.Select(x => x.Id)); + } + + builder.Add(languageAndAnalyzers.Key, new AnalyzerAssemblyContents(analyzers.ToImmutable(), supportedDiagnostics.ToImmutable())); + } + + return builder.ToImmutable(); + } +} diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs index ca46d49686..7244d4e0e6 100644 --- a/src/RoslynAnalyzerServer/Resources.Designer.cs +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -177,6 +176,24 @@ internal static string HttpServerStarting { } } + /// + /// Looks up a localized string similar to Failed to load analyzer {0}: {1}. + /// + internal static string RoslynAnalysisAnalyzerLoaderFailedToLoad { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerLoaderFailedToLoad", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Analyzer Loader. + /// + internal static string RoslynAnalysisAnalyzerLoaderLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerLoaderLogContext", resourceCulture); + } + } + /// /// Looks up a localized string similar to Configuration. /// diff --git a/src/RoslynAnalyzerServer/Resources.resx b/src/RoslynAnalyzerServer/Resources.resx index 49fec90dea..6cdb158f95 100644 --- a/src/RoslynAnalyzerServer/Resources.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -168,4 +168,10 @@ No active rules loaded for language {0} + + Analyzer Loader + + + Failed to load analyzer {0}: {1} + \ No newline at end of file diff --git a/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs b/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs index 2a63b3179d..2414878c13 100644 --- a/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs +++ b/src/SLCore.UnitTests/Configuration/SlCoreLanguageProviderTests.cs @@ -62,7 +62,7 @@ private void MockLanguageProvider() languageProvider = Substitute.For(); // it doesn't have to be the real lists, just need to be different so the test can verify that the provider is using them languageProvider.AllKnownLanguages.Returns([Language.C, Language.Js, Language.Ts, Language.TSql]); - languageProvider.RoslynLanguages.Returns([Language.C]); + languageProvider.RoslynLanguages.Returns([Language.CSharp]); languageProvider.NonRoslynLanguages.Returns([Language.Js]); languageProvider.LanguagesInStandaloneMode.Returns([Language.Ts]); languageProvider.ExtraLanguagesInConnectedMode.Returns([Language.TSql]); diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs index 71ca390fa4..3fee3df968 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs @@ -30,7 +30,7 @@ namespace SonarQube.Client.Tests; [TestClass] public class SonarQubeService_GetSuppressedRoslynIssuesAsync : SonarQubeService_GetIssuesBase { - protected override Language[] MockRoslynLanguages => [Language.CSharp, Language.VBNET, Language.Cpp]; + protected override RoslynLanguage[] MockRoslynLanguages => [Language.CSharp, Language.VBNET]; private string[] MockRoslynServerLanguageKeys => MockRoslynLanguages.Select(x => x.ServerLanguageKey).ToArray(); [TestMethod] @@ -38,7 +38,7 @@ public async Task GetSuppressedRoslynIssuesAsync_From_7_20() { await ConnectToSonarQube("7.2.0.0"); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet%2Ccpp&p=1&ps=500", @" + SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet&p=1&ps=500", @" { ""total"": 5, ""p"": 1, @@ -74,7 +74,7 @@ public async Task GetSuppressedRoslynIssuesAsync_From_7_20() ""components"": [ ] } "); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=BUG&languages=cs%2Cvbnet%2Ccpp&p=1&ps=500", @" + SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=BUG&languages=cs%2Cvbnet&p=1&ps=500", @" { ""total"": 5, ""p"": 1, @@ -162,7 +162,7 @@ public async Task GetSuppressedRoslynIssuesAsync_From_7_20_NotFound() { await ConnectToSonarQube("7.2.0.0"); - SetupRequest("api/issues/search?projects=project1&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet%2Ccpp&p=1&ps=500", "", HttpStatusCode.NotFound); + SetupRequest("api/issues/search?projects=project1&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet&p=1&ps=500", "", HttpStatusCode.NotFound); Func>> func = async () => await service.GetSuppressedRoslynIssuesAsync("project1", null, null, CancellationToken.None); diff --git a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs index 6e41791b8e..9984f8b8ea 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs @@ -51,7 +51,7 @@ public class SonarQubeService_TestBase protected const string UserAgent = "the-test-user-agent/1.0"; - protected virtual Language[] MockRoslynLanguages { get; } + protected virtual RoslynLanguage[] MockRoslynLanguages { get; } [TestInitialize] public void TestInitialize() diff --git a/src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs b/src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs new file mode 100644 index 0000000000..96d3d8a50d --- /dev/null +++ b/src/TestInfrastructure/Helpers/FakeRoslynLanguage.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core; + +namespace SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; + +public class FakeRoslynLanguage(string key) : RoslynLanguage(key, key, key, new PluginInfo(key, key), new RepoInfo(key), key, key, new RepoInfo(key, key)) +{ + public static RoslynLanguage Instance = new FakeRoslynLanguage("fakeroslyn"); +} From 5ee04be481abd8a3706cc5ffa00f9cacb4e89be9 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Tue, 26 Aug 2025 09:28:49 +0000 Subject: [PATCH 14/38] SLVS-2490 Catch exceptions thrown when handling a request (#6387) --- .../Http/RoslynAnalysisHttpServerTest.cs | 16 ++++++++++++++++ .../Http/RoslynAnalysisHttpServer.cs | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs index 780d2107d2..8fe89ec491 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs @@ -191,6 +191,22 @@ public async Task StartListenAsync_NoFilesToAnalyze_ReturnsBadRequest() response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } + [TestMethod] + public async Task StartListenAsync_AnalysisThrowsException_ReturnsInternalServerError() + { + var exceptionMessage = "Simulated exception"; + using var serverStarter2 = new HttpServerStarter(); + serverStarter2.MockedRoslynAnalysisService + .When(x => x.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), Arg.Any())) + .Do(_ => throw new InvalidOperationException(exceptionMessage)); + serverStarter2.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(CreateClientRequestConfig(serverStarter2)); + + response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + serverStarter2.MockedLogger.Received(1).LogVerbose(Resources.HttpRequestFailed, Arg.Is(x => x.Contains(exceptionMessage))); + } + [TestMethod] public async Task Dispose_StopsServer() { diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs index a722cfd25c..29ec256cfc 100644 --- a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -138,6 +138,11 @@ private async Task HandleRequestWithTimeout(IHttpListenerContext context, Cancel logger.LogVerbose(Resources.HttpRequestTimedOut, settings.RequestMillisecondsTimeout); httpRequestHandler.CloseRequest(context, HttpStatusCode.RequestTimeout); } + catch (Exception exception) + { + logger.LogVerbose(Resources.HttpRequestFailed, exception.Message + exception.StackTrace); + httpRequestHandler.CloseRequest(context, HttpStatusCode.InternalServerError); + } } private async Task HandleRequest(IHttpListenerContext context, CancellationToken cancellationToken) From 7f70fdc411841ce4e2700e94e39d3004ca308aa8 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:27:49 +0200 Subject: [PATCH 15/38] SLVS-2495 Load CodeFixProvider-s from analyzer assemblies (#6389) [SLVS-2495](https://sonarsource.atlassian.net/browse/SLVS-2495) Part of SLVS-2406 [SLVS-2495]: https://sonarsource.atlassian.net/browse/SLVS-2495?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- ...oslynAnalysisConfigurationProviderTests.cs | 10 ++- .../RoslynAnalysisProfilesProviderTests.cs | 25 +++--- .../RoslynAnalyzerProviderTests.cs | 85 ++++++++++++++++--- .../SonarLintXmlProviderTests.cs | 10 +-- .../RoslynProjectCompilationProviderTests.cs | 11 ++- .../RoslynAnalyzerServer.UnitTests.csproj | 2 + .../packages.lock.json | 66 ++++++++++++++ .../IRoslynAnalysisProfilesProvider.cs | 9 +- .../Configuration/IRoslynAnalyzerLoader.cs | 5 +- .../Configuration/IRoslynAnalyzerProvider.cs | 8 +- .../RoslynAnalysisConfigurationProvider.cs | 5 +- .../RoslynAnalysisProfilesProvider.cs | 18 ++-- .../Configuration/RoslynAnalyzerLoader.cs | 46 ++++++++-- .../Configuration/RoslynAnalyzerProvider.cs | 33 +++++-- .../Analysis/RoslynAnalysisConfiguration.cs | 4 +- .../Resources.Designer.cs | 9 ++ src/RoslynAnalyzerServer/Resources.resx | 3 + 17 files changed, 285 insertions(+), 64 deletions(-) diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs index 6ef1173aba..7d621a3455 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -19,6 +19,7 @@ */ using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; @@ -46,7 +47,7 @@ public void TestInitialize() { sonarLintXmlProvider = Substitute.For(); roslynAnalyzerProvider = Substitute.For(); - roslynAnalyzerProvider.LoadAnalyzerAssemblies().Returns(DefaultAnalyzers); + roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies().Returns(DefaultAnalyzers); analyzerProfilesProvider = Substitute.For(); testLogger = Substitute.ForPartsOf(); @@ -83,12 +84,14 @@ public void GetConfiguration_CreatesConfigurationForEachLanguage() { Language.CSharp, new RoslynAnalysisProfile( CreateTestAnalyzers(1), + CreateTestCodeFixProviders(), [CreateRuleConfiguration(Language.CSharp, "S001"), CreateRuleConfiguration(Language.CSharp, "S002", false)], new() { { "sonar.cs.property", "value" } }) }, { Language.VBNET, new RoslynAnalysisProfile( CreateTestAnalyzers(2), + CreateTestCodeFixProviders(), [CreateRuleConfiguration(Language.VBNET, "S001", false), CreateRuleConfiguration(Language.VBNET, "S002")], new() { { "sonar.vbnet.property", "value" } }) } @@ -105,6 +108,7 @@ public void GetConfiguration_CreatesConfigurationForEachLanguage() { result.ContainsKey(language).Should().BeTrue(); result[language].Analyzers.Should().BeEquivalentTo(roslynAnalysisProfiles[language].Analyzers); + result[language].CodeFixProvidersByRuleKey.Should().BeSameAs(roslynAnalysisProfiles[language].CodeFixProvidersByRuleKey); result[language].DiagnosticOptions.Should().BeEquivalentTo(roslynAnalysisProfiles[language].Rules.ToDictionary(x => x.RuleId.RuleKey, x => x.ReportDiagnostic)); result[language].SonarLintXml.Should().BeEquivalentTo(xmlConfigurations[language]); } @@ -119,6 +123,7 @@ public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() { language, new RoslynAnalysisProfile( ImmutableArray.Empty, + CreateTestCodeFixProviders(), [CreateRuleConfiguration(language, "S001")], new Dictionary()) } @@ -142,6 +147,7 @@ public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() { language, new RoslynAnalysisProfile( CreateTestAnalyzers(1), + CreateTestCodeFixProviders(), [CreateRuleConfiguration(language, "S001", false), CreateRuleConfiguration(language, "S002", false)], new Dictionary()) } @@ -188,6 +194,8 @@ private RoslynRuleConfiguration CreateRuleConfiguration( private ImmutableArray CreateTestAnalyzers(int count) => Enumerable.Range(0, count).Select(_ => Substitute.For()).ToImmutableArray(); + private ImmutableDictionary> CreateTestCodeFixProviders() => ImmutableDictionary>.Empty.Add("any", [Substitute.For()]); + private SonarLintXmlConfigurationFile SetUpXmlProvider(RoslynAnalysisProfile profile) { var slxml = new SonarLintXmlConfigurationFile("any", "any"); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs index 369ecfdb1a..1142a04b38 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisProfilesProviderTests.cs @@ -19,6 +19,7 @@ */ using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; @@ -56,10 +57,10 @@ public void GetAnalysisProfilesByLanguage_EmptyInputs_ReturnsEmptyDictionary() [TestMethod] public void GetAnalysisProfilesByLanguage_ReturnsFilteredRulesAndParameters() { - var supportedDiagnostics = CreateSupportedDiagnosticsForLanguages(new() + var analyzerAssemblyContents = CreateSupportedDiagnosticsForLanguages(new() { - { Language.CSharp, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"]) }, - { Language.VBNET, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"]) } + { Language.CSharp, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"], new(){{"S002", [Substitute.For()]}}) }, + { Language.VBNET, ([Substitute.For(), Substitute.For()], ["S001", "S002", "S003"], new (){{"S003", [Substitute.For()]}}) }, }); List activeRules = [ @@ -70,12 +71,13 @@ public void GetAnalysisProfilesByLanguage_ReturnsFilteredRulesAndParameters() ]; var analysisProperties = new Dictionary { { "sonar.cs.property1", "value1" }, { "sonar.vbnet.property2", "value2" }, { "someotherkey", "value" } }; - var result = testSubject.GetAnalysisProfilesByLanguage(supportedDiagnostics, activeRules, analysisProperties); + var result = testSubject.GetAnalysisProfilesByLanguage(analyzerAssemblyContents, activeRules, analysisProperties); result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); ValidateProfile( result[Language.CSharp], - supportedDiagnostics[Language.CSharp].Analyzers, + analyzerAssemblyContents[Language.CSharp].Analyzers, + analyzerAssemblyContents[Language.CSharp].CodeFixProvidersByRuleKey, [ CreateRuleConfiguration(Language.CSharp, "S001", new() { { "param1", "value1" } }), CreateRuleConfiguration(Language.CSharp, "S002", isActive: false), @@ -84,7 +86,8 @@ public void GetAnalysisProfilesByLanguage_ReturnsFilteredRulesAndParameters() new() { { "sonar.cs.property1", "value1" } }); ValidateProfile( result[Language.VBNET], - supportedDiagnostics[Language.VBNET].Analyzers, + analyzerAssemblyContents[Language.VBNET].Analyzers, + analyzerAssemblyContents[Language.VBNET].CodeFixProvidersByRuleKey, [ CreateRuleConfiguration(Language.VBNET, "S001", isActive: false), CreateRuleConfiguration(Language.VBNET, "S002", parameters: new() { { "param2", "value2" } }), @@ -93,8 +96,8 @@ public void GetAnalysisProfilesByLanguage_ReturnsFilteredRulesAndParameters() new Dictionary { { "sonar.vbnet.property2", "value2" } }); } - private static void ValidateProfile(RoslynAnalysisProfile profile, IEnumerable diagnosticAnalyzers, List rules, Dictionary analysisProperties) => - profile.Should().BeEquivalentTo(new RoslynAnalysisProfile(diagnosticAnalyzers.ToImmutableArray(), rules, analysisProperties), options => options.ComparingByMembers().ComparingByMembers()); + private static void ValidateProfile(RoslynAnalysisProfile profile, IEnumerable diagnosticAnalyzers, ImmutableDictionary> codeFixProviders, List rules, Dictionary analysisProperties) => + profile.Should().BeEquivalentTo(new RoslynAnalysisProfile(diagnosticAnalyzers.ToImmutableArray(), codeFixProviders, rules, analysisProperties), options => options.ComparingByMembers().ComparingByMembers()); private static RoslynRuleConfiguration CreateRuleConfiguration( Language language, @@ -106,8 +109,8 @@ private static RoslynRuleConfiguration CreateRuleConfiguration( parameters); private static ImmutableDictionary CreateSupportedDiagnosticsForLanguages( - Dictionary analyzers) => - analyzers.ToImmutableDictionary( + Dictionary> CodeFixProviders)> contents) => + contents.ToImmutableDictionary( x => x.Key, - y => new AnalyzerAssemblyContents(y.Value.analyzers.ToImmutableArray(), y.Value.RuleKeys.ToImmutableHashSet())); + y => new AnalyzerAssemblyContents(y.Value.analyzers.ToImmutableArray(), y.Value.RuleKeys.ToImmutableHashSet(), y.Value.CodeFixProviders.ToImmutableDictionary())); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs index 1944fd9d32..0376ec6fb3 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs @@ -20,9 +20,9 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.TestInfrastructure; @@ -60,7 +60,7 @@ public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() { analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary>()); - var result = testSubject.LoadAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); result.Should().BeEmpty(); } @@ -68,32 +68,41 @@ public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() [TestMethod] public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrectDictionary() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage() + .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("CS0001"); var vbAnalyzer = CreateAnalyzerWithDiagnostic("VB0001"); - roslynAnalyzerLoader.LoadAnalyzers(CsharpAnalyzerPath).Returns([csharpAnalyzer]); - roslynAnalyzerLoader.LoadAnalyzers(VbAnalyzerPath).Returns([vbAnalyzer]); + var csharpCodeFixer = CreateCodeFixProviderWithDiagnostics("CS0001"); + var vbCodeFixer = CreateCodeFixProviderWithDiagnostics("VB0001"); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [csharpCodeFixer])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([vbAnalyzer], [vbCodeFixer])); - var result = testSubject.LoadAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer); result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("CS0001"); + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "CS0001", [csharpCodeFixer] } }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(vbAnalyzer); result[Language.VBNET].SupportedRuleKeys.Should().BeEquivalentTo("VB0001"); + result[Language.VBNET].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "VB0001", [vbCodeFixer] } }); } [TestMethod] public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage() + .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001", "SDUPLICATE"); var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "SDUPLICATE"); var vbAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002"); - roslynAnalyzerLoader.LoadAnalyzers(CsharpAnalyzerPath).Returns([csharpAnalyzer1, csharpAnalyzer2]); - roslynAnalyzerLoader.LoadAnalyzers(VbAnalyzerPath).Returns([vbAnalyzer]); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer1, csharpAnalyzer2], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([vbAnalyzer], [])); - var result = testSubject.LoadAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "SDUPLICATE", "S002"); @@ -108,16 +117,59 @@ public void GetAnalyzersByLanguage_MultipleAnalyzersPerLanguage_CombinesAllRules var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001"); var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "S003"); var csharpAnalyzer3 = CreateAnalyzerWithDiagnostic("S004"); - roslynAnalyzerLoader.LoadAnalyzers(CsharpAnalyzerPath).Returns([csharpAnalyzer1]); - roslynAnalyzerLoader.LoadAnalyzers(csharpAnalyzerPath2).Returns([csharpAnalyzer2, csharpAnalyzer3]); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer1], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(csharpAnalyzerPath2).Returns(new LoadedAnalyzerClasses([csharpAnalyzer2, csharpAnalyzer3], [])); - var result = testSubject.LoadAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); result.Keys.Should().BeEquivalentTo(Language.CSharp); result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer1, csharpAnalyzer2, csharpAnalyzer3); result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "S002", "S003", "S004"); } + [TestMethod] + public void GetAnalyzersByLanguage_NoCodeFixProviders_ReturnsEmptyMap() + { + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [])); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEmpty(); + } + + [TestMethod] + public void GetAnalyzersByLanguage_CodeFixProviderWithMultipleDiagnostics_AddedToAllMappings() + { + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002", "S003"); + var codeFixProvider = CreateCodeFixProviderWithDiagnostics("S001", "S002"); + + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [codeFixProvider])); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "S001", [codeFixProvider] }, { "S002", [codeFixProvider] } }); + } + + [TestMethod] + public void GetAnalyzersByLanguage_MultipleCodeFixProvidersForSameId_AllAddedToSameCollection() + { + analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); + var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); + var codeFixProvider1 = CreateCodeFixProviderWithDiagnostics("S001"); + var codeFixProvider2 = CreateCodeFixProviderWithDiagnostics("S001"); + + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [codeFixProvider1, codeFixProvider2])); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + + result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( + new Dictionary> { { "S001", [codeFixProvider1, codeFixProvider2] } }); + } + private static DiagnosticAnalyzer CreateAnalyzerWithDiagnostic(params string[] diagnosticIds) { var analyzer = Substitute.For(); @@ -125,6 +177,13 @@ private static DiagnosticAnalyzer CreateAnalyzerWithDiagnostic(params string[] d return analyzer; } + private static CodeFixProvider CreateCodeFixProviderWithDiagnostics(params string[] diagnosticIds) + { + var codeFixProvider = Substitute.For(); + codeFixProvider.FixableDiagnosticIds.Returns(diagnosticIds.ToImmutableArray()); + return codeFixProvider; + } + private static DiagnosticDescriptor CreateDiagnosticDescriptor(string id) => new( id, diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs index 125ad3e7cd..3961faa936 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/SonarLintXmlProviderTests.cs @@ -51,7 +51,7 @@ public void TestInitialize() [TestMethod] public void Create_ReturnsExpectedXml() { - var profile = new RoslynAnalysisProfile(default, [], []); + var profile = new RoslynAnalysisProfile(default, null!, [], []); var result = testSubject.Create(profile); @@ -65,7 +65,7 @@ public void Create_ReturnsExpectedXml() public void Create_WithMultipleRulesAndProperties_ExpectedConfigurationSerialized() { var analysisProperties = new Dictionary { { "prop1", "value1" }, { "prop2", "value2" } }; - var result = testSubject.Create(new RoslynAnalysisProfile(default, [RuleWithoutParameters, RuleWithParameters], analysisProperties)); + var result = testSubject.Create(new RoslynAnalysisProfile(default, null!, [RuleWithoutParameters, RuleWithParameters], analysisProperties)); result.Should().NotBeNull(); @@ -80,7 +80,7 @@ public void Create_WithMultipleRulesAndProperties_ExpectedConfigurationSerialize [TestMethod] public void Create_WithRuleNoParametersNoProperties_SerializesCorrectRules() { - var profile = new RoslynAnalysisProfile(default, [RuleWithoutParameters], []); + var profile = new RoslynAnalysisProfile(default, null!, [RuleWithoutParameters], []); var result = testSubject.Create(profile); @@ -92,7 +92,7 @@ public void Create_WithRuleNoParametersNoProperties_SerializesCorrectRules() [TestMethod] public void Create_WithRuleWithParameters_SerializesCorrectRules() { - var profile = new RoslynAnalysisProfile(default, [RuleWithParameters], []); + var profile = new RoslynAnalysisProfile(default, null!, [RuleWithParameters], []); var result = testSubject.Create(profile); @@ -116,7 +116,7 @@ public void Create_WithInactiveRules_OnlyIncludesActiveRules() { const string inactiveRuleKey = "inactiveRule"; var rules = new List { RuleWithoutParameters, CreateRuleConfig(inactiveRuleKey, false), RuleWithParameters }; - var profile = new RoslynAnalysisProfile(default, rules, []); + var profile = new RoslynAnalysisProfile(default, null!, rules, []); var result = testSubject.Create(profile); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs index 8f3811caa0..1ddafad62d 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs @@ -20,6 +20,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; @@ -38,6 +39,7 @@ public class RoslynProjectCompilationProviderTests private DiagnosticAnalyzer analyzer3 = null!; private AnalyzerOptions analyzerOptions = null!; private ImmutableArray analyzers; + private ImmutableDictionary> codeFixProviders = null!; private IRoslynCompilationWrapper compilation = null!; private CompilationOptions compilationOptions = null!; private IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers = null!; @@ -58,13 +60,15 @@ public void TestInitialize() SetUpAdditionalFiles(); SetUpProject(); SetUpAnalyzers(); + SetUpCodeFixProviders(); diagnosticOptions = ImmutableDictionary.Empty .Add("SomeId", ReportDiagnostic.Warn); configurations = ImmutableDictionary.Empty .Add(Language.CSharp, new RoslynAnalysisConfiguration( sonarLintXml, diagnosticOptions, - analyzers)); + analyzers, + codeFixProviders)); testSubject = new RoslynProjectCompilationProvider(logger); } @@ -147,6 +151,11 @@ private void SetUpAnalyzers() analyzers = ImmutableArray.Create(analyzer1, analyzer2, analyzer3); } + private void SetUpCodeFixProviders() + { + codeFixProviders = ImmutableDictionary>.Empty.Add("1", [Substitute.For()]); + } + private void SetUpProject() { project = Substitute.For(); diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj index 565b4d9b43..d18684008d 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalyzerServer.UnitTests.csproj @@ -7,6 +7,7 @@ SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests enable enable + true @@ -17,6 +18,7 @@ + diff --git a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json index 4d06016b56..7acc230628 100644 --- a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json +++ b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json @@ -38,6 +38,19 @@ "Microsoft.CodeAnalysis.Common": "[3.11.0]" } }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", + "dependencies": { + "Humanizer.Core": "2.2.0", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "System.Composition": "1.0.31", + "System.IO.Pipelines": "5.0.1" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -145,6 +158,11 @@ "resolved": "1.4.1", "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" + }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -994,6 +1012,54 @@ "resolved": "4.5.0", "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" }, + "System.Composition": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Convention": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31", + "System.Composition.TypedParts": "1.0.31" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", + "dependencies": { + "System.Composition.Runtime": "1.0.31" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31" + } + }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "5.0.1", diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs index 4827641be4..c13035f0cf 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisProfilesProvider.cs @@ -19,6 +19,7 @@ */ using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; @@ -28,9 +29,13 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalysisProfilesProvider { Dictionary GetAnalysisProfilesByLanguage( - ImmutableDictionary supportedRulesByLanguage, + ImmutableDictionary analyzerAssemblyContents, List activeRules, Dictionary? analysisProperties); } -internal record struct RoslynAnalysisProfile(ImmutableArray Analyzers, List Rules, Dictionary AnalysisProperties); +internal record struct RoslynAnalysisProfile( + ImmutableArray Analyzers, + ImmutableDictionary> CodeFixProvidersByRuleKey, + List Rules, + Dictionary AnalysisProperties); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs index 8293cac7b5..84e7663d74 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerLoader.cs @@ -18,11 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalyzerLoader { - IReadOnlyCollection LoadAnalyzers(string filePath); + LoadedAnalyzerClasses LoadAnalyzerAssembly(string filePath); } + +internal readonly record struct LoadedAnalyzerClasses(IReadOnlyCollection Analyzers, IReadOnlyCollection CodeFixProviders); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs index 3cf403e178..00113c3a70 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs @@ -19,6 +19,7 @@ */ using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; @@ -26,7 +27,10 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalyzerProvider { - ImmutableDictionary LoadAnalyzerAssemblies(); + ImmutableDictionary LoadAndProcessAnalyzerAssemblies(); } -internal record struct AnalyzerAssemblyContents(ImmutableArray Analyzers, ImmutableHashSet SupportedRuleKeys); +internal readonly record struct AnalyzerAssemblyContents( + ImmutableArray Analyzers, + ImmutableHashSet SupportedRuleKeys, + ImmutableDictionary> CodeFixProvidersByRuleKey); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs index 83b279fb96..120b6eb8b1 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -40,7 +40,7 @@ public IReadOnlyDictionary GetConfigurati { // todo add caching https://sonarsource.atlassian.net/browse/SLVS-2481 - var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.LoadAnalyzerAssemblies(), activeRules, analysisProperties); + var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(), activeRules, analysisProperties); var configurations = new Dictionary(); foreach (var analyzerAndLanguage in analysisProfilesByLanguage) @@ -67,7 +67,8 @@ public IReadOnlyDictionary GetConfigurati new RoslynAnalysisConfiguration( sonarLintXmlProvider.Create(analysisProfile), analysisProfile.Rules.ToImmutableDictionary(x => x.RuleId.RuleKey, y => y.ReportDiagnostic), - analysisProfile.Analyzers)); + analysisProfile.Analyzers, + analysisProfile.CodeFixProvidersByRuleKey)); } return configurations; diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs index 3e145b53e6..ac809ab53f 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisProfilesProvider.cs @@ -30,22 +30,21 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal class RoslynAnalysisProfilesProvider : IRoslynAnalysisProfilesProvider { public Dictionary GetAnalysisProfilesByLanguage( - ImmutableDictionary supportedRulesByLanguage, + ImmutableDictionary analyzerAssemblyContents, List activeRules, Dictionary? analysisProperties) { - var roslynAnalysisProfiles = InitializeProfilesForEachLanguage(supportedRulesByLanguage); - AddRules(activeRules, supportedRulesByLanguage, roslynAnalysisProfiles); + var roslynAnalysisProfiles = InitializeProfilesForEachLanguage(analyzerAssemblyContents); + AddRules(activeRules, analyzerAssemblyContents, roslynAnalysisProfiles); AddProperties(analysisProperties, roslynAnalysisProfiles); return roslynAnalysisProfiles; } - private static Dictionary InitializeProfilesForEachLanguage(ImmutableDictionary supportedRulesByLanguage) - { - var roslynAnalysisProfiles = supportedRulesByLanguage.ToDictionary(x => x.Key, x => new RoslynAnalysisProfile(x.Value.Analyzers, [], [])); - return roslynAnalysisProfiles; - } + private static Dictionary InitializeProfilesForEachLanguage(ImmutableDictionary analyzerAssemblyContents) => + analyzerAssemblyContents.ToDictionary( + x => x.Key, + x => new RoslynAnalysisProfile(x.Value.Analyzers, x.Value.CodeFixProvidersByRuleKey, [], [])); private static void AddRules( List activeRules, @@ -63,9 +62,8 @@ private static void AddRules( continue; } - foreach (var ruleKey in kvp.Value.SupportedRuleKeys) + foreach (var ruleId in kvp.Value.SupportedRuleKeys.Select(ruleKey => new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey))) { - var ruleId = new SonarCompositeRuleId(language.RepoInfo.Key, ruleKey); analysisProfile.Rules.Add(activeRulesById.TryGetValue(ruleId.ErrorListErrorCode, out var activeRule) ? new RoslynRuleConfiguration(ruleId, true, activeRule.Parameters) : new RoslynRuleConfiguration(ruleId, false, null)); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs index 6acb5a046a..c2f51f098e 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; @@ -29,25 +30,54 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [Export(typeof(IRoslynAnalyzerLoader))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] +[ExcludeFromCodeCoverage] internal class RoslynAnalyzerLoader(ILogger logger) : IRoslynAnalyzerLoader { private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); - [ExcludeFromCodeCoverage] - public IReadOnlyCollection LoadAnalyzers(string filePath) + public LoadedAnalyzerClasses LoadAnalyzerAssembly(string filePath) { try { - return Assembly.LoadFrom(filePath) - .GetTypes() - .Where(t => typeof(DiagnosticAnalyzer).IsAssignableFrom(t) && !t.IsAbstract) - .Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t)) - .ToList(); + var analyzers = new List(); + var codeFixProviders = new List(); + + foreach (var type in Assembly.LoadFrom(filePath).GetTypes().Where(t => t is { IsAbstract: false, IsInterface: false, IsGenericType: false })) + { + if (TryLoadType(type, out CodeFixProvider? codeFixProvider, filePath)) + { + codeFixProviders.Add(codeFixProvider); + } + else if (TryLoadType(type, out DiagnosticAnalyzer? analyzer, filePath)) + { + analyzers.Add(analyzer); + } + } + + return new LoadedAnalyzerClasses(analyzers, codeFixProviders); } catch (Exception e) { logger.WriteLine(Resources.RoslynAnalysisAnalyzerLoaderFailedToLoad, filePath, e); - return []; + return new LoadedAnalyzerClasses([], []); + } + } + + private bool TryLoadType(Type type, [NotNullWhen(true)] out T? value, string originAssembly) where T : class + { + value = null; + try + { + if (typeof(T).IsAssignableFrom(type)) + { + value = (T)Activator.CreateInstance(type); + return true; + } + } + catch (Exception e) + { + logger.LogVerbose(Resources.RoslynAnalysisAnalyzerClassLoaderFailedToLoad, type, originAssembly, e); } + return false; } } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs index 116d03bffc..b17ad10eaa 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs @@ -20,6 +20,7 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.CSharpVB; @@ -31,11 +32,11 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [method: ImportingConstructor] internal class RoslynAnalyzerProvider(IEmbeddedDotnetAnalyzersLocator analyzersLocator, IRoslynAnalyzerLoader roslynAnalyzerLoader) : IRoslynAnalyzerProvider { - public ImmutableDictionary LoadAnalyzerAssemblies() => + public ImmutableDictionary LoadAndProcessAnalyzerAssemblies() => // todo SLVS-2410 Respect NET repackaging - LoadAnalyzersAndRules(analyzersLocator.GetBasicAnalyzerFullPathsByLanguage()); + LoadFromAssemblies(analyzersLocator.GetBasicAnalyzerFullPathsByLanguage()); - private ImmutableDictionary LoadAnalyzersAndRules(Dictionary> analyzerFullPathsByLanguage) + private ImmutableDictionary LoadFromAssemblies(Dictionary> analyzerFullPathsByLanguage) { var builder = ImmutableDictionary.CreateBuilder(); @@ -43,16 +44,34 @@ private ImmutableDictionary LoadAnalyz { var supportedDiagnostics = ImmutableHashSet.CreateBuilder(); var analyzers = ImmutableArray.CreateBuilder(); + var codeFixProviders = ImmutableDictionary.CreateBuilder>(); - foreach (var diagnosticAnalyzer in languageAndAnalyzers.Value.SelectMany(roslynAnalyzerLoader.LoadAnalyzers)) + foreach (var assemblyContents in languageAndAnalyzers.Value.Select(roslynAnalyzerLoader.LoadAnalyzerAssembly)) { - analyzers.Add(diagnosticAnalyzer); - supportedDiagnostics.UnionWith(diagnosticAnalyzer.SupportedDiagnostics.Select(x => x.Id)); + analyzers.AddRange(assemblyContents.Analyzers); + supportedDiagnostics.UnionWith(assemblyContents.Analyzers.SelectMany(x => x.SupportedDiagnostics.Select(y => y.Id))); + AddCodeFixProviders(assemblyContents, codeFixProviders); } - builder.Add(languageAndAnalyzers.Key, new AnalyzerAssemblyContents(analyzers.ToImmutable(), supportedDiagnostics.ToImmutable())); + var immutableArray = supportedDiagnostics.ToImmutable(); + builder.Add(languageAndAnalyzers.Key, new AnalyzerAssemblyContents(analyzers.ToImmutable(), immutableArray.ToImmutableHashSet(), codeFixProviders.ToImmutable())); } return builder.ToImmutable(); } + + private static void AddCodeFixProviders(LoadedAnalyzerClasses classes, ImmutableDictionary>.Builder codeFixProviders) + { + foreach (var codeFixProvider in classes.CodeFixProviders) + { + foreach (var fixableDiagnosticId in codeFixProvider.FixableDiagnosticIds) + { + if (!codeFixProviders.ContainsKey(fixableDiagnosticId)) + { + codeFixProviders[fixableDiagnosticId] = new List(); + } + ((List)codeFixProviders[fixableDiagnosticId]).Add(codeFixProvider); + } + } + } } diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs index 6b2c1d6799..e285ece8bc 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynAnalysisConfiguration.cs @@ -20,6 +20,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; @@ -28,4 +29,5 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; internal record struct RoslynAnalysisConfiguration( SonarLintXmlConfigurationFile SonarLintXml, ImmutableDictionary DiagnosticOptions, - ImmutableArray Analyzers); + ImmutableArray Analyzers, + ImmutableDictionary> CodeFixProvidersByRuleKey); diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs index 7244d4e0e6..ee2edd4071 100644 --- a/src/RoslynAnalyzerServer/Resources.Designer.cs +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -176,6 +176,15 @@ internal static string HttpServerStarting { } } + /// + /// Looks up a localized string similar to Failed to load class {0} from {1}: {2}. + /// + internal static string RoslynAnalysisAnalyzerClassLoaderFailedToLoad { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerClassLoaderFailedToLoad", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to load analyzer {0}: {1}. /// diff --git a/src/RoslynAnalyzerServer/Resources.resx b/src/RoslynAnalyzerServer/Resources.resx index 6cdb158f95..41bce7f135 100644 --- a/src/RoslynAnalyzerServer/Resources.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -174,4 +174,7 @@ Failed to load analyzer {0}: {1} + + Failed to load class {0} from {1}: {2} + \ No newline at end of file From c7ee9f87147e9cba04a2a020b02ca3ad999a863d Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Thu, 28 Aug 2025 15:15:32 +0000 Subject: [PATCH 16/38] SLVS-2500 Respect NET repackaging (#6390) --- .../EmbeddedDotnetAnalyzersLocatorTests.cs | 73 +++++++------------ .../EmbeddedDotnetAnalyzersLocator.cs | 42 +++++------ .../Http/Helper/HttpServerStarter.cs | 3 +- .../Http/RoslynAnalysisHttpServerTest.cs | 5 +- ...oslynAnalysisConfigurationProviderTests.cs | 11 +-- .../RoslynAnalyzerProviderTests.cs | 31 ++++---- .../RoslynAnalysisServiceTests.cs | 21 ++++-- .../IEmbeddedRoslynAnalyzersLocator.cs | 5 +- .../IRoslynAnalysisConfigurationProvider.cs | 2 +- .../Configuration/IRoslynAnalyzerProvider.cs | 3 +- .../RoslynAnalysisConfigurationProvider.cs | 4 +- .../Configuration/RoslynAnalyzerProvider.cs | 7 +- .../Http/Models/AnalysisRequest.cs | 1 + .../Http/Models/AnalyzerInfoDto.cs | 23 ++++++ .../Http/RoslynAnalysisHttpServer.cs | 2 +- .../IRoslynAnalysisService.cs | 4 +- .../RoslynAnalysisService.cs | 14 ++-- 17 files changed, 129 insertions(+), 122 deletions(-) create mode 100644 src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs diff --git a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs index 338dbb5e0c..7684daf2cb 100644 --- a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs @@ -20,11 +20,11 @@ using System.IO; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.Integration.UnitTests.EmbeddedAnalyzers; @@ -37,36 +37,27 @@ public class EmbeddedDotnetAnalyzersLocatorTests private static readonly string VbRegularAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.VisualBasic.dll"); private static readonly string CSharpEnterpriseAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.CSharp.dll"); private static readonly string VbEnterpriseAnalyzer = GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.VisualBasic.dll"); + private IFileSystemService fileSystem; private EmbeddedDotnetAnalyzersLocator testSubject; private IVsixRootLocator vsixRootLocator; - private IFileSystemService fileSystem; - private ILanguageProvider languageProvider; [TestInitialize] public void TestInitialize() { vsixRootLocator = Substitute.For(); - languageProvider = Substitute.For(); - languageProvider.RoslynLanguages.Returns([Language.CSharp, Language.VBNET]); fileSystem = Substitute.For(); - testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, languageProvider, fileSystem); + testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, fileSystem); } [TestMethod] - public void MefCtor_CheckIsExported() - { + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); - } [TestMethod] - public void MefCtor_IsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } + public void MefCtor_IsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] public void GetBasicAnalyzerFullPaths_AnalyzersExists_ReturnsFullPathsToAnalyzers() @@ -149,30 +140,21 @@ public void GetAnalyzerFullPaths_SearchesForFilesInsideVsix() } [TestMethod] - public void GetBasicAnalyzerFullPathsByLanguage_GroupsDllsByLanguageAndFiltersEnterprise() + public void GetAnalyzerFullPathsByLanguage_BothEnterprise_GroupsEnterpriseDllsByLanguage() { fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ CSharpRegularAnalyzer, VbRegularAnalyzer, - CSharpEnterpriseAnalyzer + CSharpEnterpriseAnalyzer, + VbEnterpriseAnalyzer ]); - testSubject.GetBasicAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> - { - [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [VbRegularAnalyzer] - }); + testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(true, true)).Should().BeEquivalentTo( + new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], [Language.VBNET] = [VbRegularAnalyzer, VbEnterpriseAnalyzer] }); } [TestMethod] - public void GetBasicAnalyzerFullPathsByLanguage_IncludesAllLanguagesEvenWithNoAnalyzers() - { - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([CSharpRegularAnalyzer]); - - testSubject.GetBasicAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [] }); - } - - [TestMethod] - public void GetEnterpriseAnalyzerFullPathsByLanguage_GroupsDllsByLanguageIncludingEnterprise() + public void GetAnalyzerFullPathsByLanguage_BothBasic_GroupsBasicDllsByLanguage() { fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ CSharpRegularAnalyzer, @@ -181,22 +163,26 @@ public void GetEnterpriseAnalyzerFullPathsByLanguage_GroupsDllsByLanguageIncludi VbEnterpriseAnalyzer ]); - testSubject.GetEnterpriseAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> - { - [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], [Language.VBNET] = [VbRegularAnalyzer, VbEnterpriseAnalyzer] - }); + testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(false, false)).Should().BeEquivalentTo( + new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [VbRegularAnalyzer] }); } [TestMethod] - public void GetEnterpriseAnalyzerFullPathsByLanguage_IncludesAllLanguagesEvenWithNoAnalyzers() + public void GetAnalyzerFullPathsByLanguage_OnlyCsharpEnterprise_GroupsDllsByLanguage() { - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([VbEnterpriseAnalyzer]); + fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ + CSharpRegularAnalyzer, + VbRegularAnalyzer, + CSharpEnterpriseAnalyzer, + VbEnterpriseAnalyzer + ]); - testSubject.GetEnterpriseAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> { [Language.CSharp] = [], [Language.VBNET] = [VbEnterpriseAnalyzer] }); + testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(true, false)).Should().BeEquivalentTo( + new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], [Language.VBNET] = [VbRegularAnalyzer] }); } [TestMethod] - public void GetEnterpriseAnalyzerFullPathsByLanguage_ExcludesLanguagesNotInRoslynLanguages() + public void GetAnalyzerFullPathsByLanguage_OnlyVbEnterprise_GroupsDllsByLanguage() { fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ CSharpRegularAnalyzer, @@ -204,17 +190,10 @@ public void GetEnterpriseAnalyzerFullPathsByLanguage_ExcludesLanguagesNotInRosly CSharpEnterpriseAnalyzer, VbEnterpriseAnalyzer ]); - // Only C# is in the Roslyn languages, VB.NET is not - languageProvider.RoslynLanguages.Returns([Language.CSharp]); - testSubject.GetEnterpriseAnalyzerFullPathsByLanguage().Should().BeEquivalentTo(new Dictionary> - { - [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer] - }); + testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(false, true)).Should().BeEquivalentTo( + new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [VbRegularAnalyzer, VbEnterpriseAnalyzer] }); } - private static string GetAnalyzerFullPath(string pathInsideVsix, string analyzerFile) - { - return Path.Combine(pathInsideVsix, analyzerFile); - } + private static string GetAnalyzerFullPath(string pathInsideVsix, string analyzerFile) => Path.Combine(pathInsideVsix, analyzerFile); } diff --git a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs index 9a4592e42e..a20e161779 100644 --- a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs +++ b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs @@ -22,11 +22,11 @@ using System.IO; using System.IO.Abstractions; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; @@ -34,7 +34,8 @@ namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; [Export(typeof(IObsoleteDotnetAnalyzersLocator))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, ILanguageProvider languageProvider, IFileSystemService fileSystem) : IEmbeddedDotnetAnalyzersLocator, IObsoleteDotnetAnalyzersLocator +internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, IFileSystemService fileSystem) + : IEmbeddedDotnetAnalyzersLocator, IObsoleteDotnetAnalyzersLocator { private const string PathInsideVsix = "EmbeddedDotnetAnalyzerDLLs"; private const string DllsSearchPattern = "SonarAnalyzer.*.dll"; // starting from 10.0, the analyzer assemblies are merged and all of the dll names start with SonarAnalyzer @@ -44,32 +45,29 @@ internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, public List GetBasicAnalyzerFullPaths() => GetBasicAnalyzerDlls().ToList(); - public Dictionary> GetBasicAnalyzerFullPathsByLanguage() => GroupByLanguage(GetBasicAnalyzerDlls()); + public Dictionary> GetAnalyzerFullPathsByLanguage(AnalyzerInfoDto analyzerInfoDto) + { + var allAnalyzers = GetAllAnalyzerDlls(); + var languageToDllsMap = new Dictionary> + { + { RoslynLanguage.CSharp, GetAnalyzerFullPathsByLanguage(RoslynLanguage.CSharp, analyzerInfoDto.ShouldUseCsharpEnterprise, allAnalyzers) }, + { RoslynLanguage.VBNET, GetAnalyzerFullPathsByLanguage(RoslynLanguage.VBNET, analyzerInfoDto.ShouldUseVbEnterprise, allAnalyzers) } + }; + return languageToDllsMap; + } + + private static List GetAnalyzerFullPathsByLanguage(RoslynLanguage language, bool shouldUseEnterprise, IEnumerable allAnalyzers) + { + var dlls = shouldUseEnterprise ? allAnalyzers : allAnalyzers.Where(x => !x.Contains(EnterpriseInfix)); + + return dlls.Where(dll => dll.Contains(language.RoslynDllIdentifier)).ToList(); + } private IEnumerable GetBasicAnalyzerDlls() => GetAllAnalyzerDlls().Where(x => !x.Contains(EnterpriseInfix)); public List GetEnterpriseAnalyzerFullPaths() => GetAllAnalyzerDlls().ToList(); - public Dictionary> GetEnterpriseAnalyzerFullPathsByLanguage() => GroupByLanguage(GetAllAnalyzerDlls()); - private string[] GetAllAnalyzerDlls() => fileSystem.Directory.GetFiles(GetPathToParentFolder(), DllsSearchPattern); private string GetPathToParentFolder() => Path.Combine(vsixRootLocator.GetVsixRoot(), PathInsideVsix); - - - private Dictionary> GroupByLanguage(IEnumerable analyzerDlls) - { - var languageToAnalyzerLocations = languageProvider.RoslynLanguages.ToDictionary(x => x, _ => new List()); - - foreach (var analyzerDll in analyzerDlls) - { - var dllLanguage = languageToAnalyzerLocations.Keys.FirstOrDefault(x => analyzerDll.Contains(x.RoslynDllIdentifier)); - if (dllLanguage != null) - { - languageToAnalyzerLocations[dllLanguage].Add(analyzerDll); - } - } - - return languageToAnalyzerLocations; - } } diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs index 2c3b55214f..b2345e84da 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs @@ -22,7 +22,6 @@ using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; -using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; @@ -76,7 +75,7 @@ private static ILogger CreateMockedLogger() private static IRoslynAnalysisService CreateMockedAnalysisEngine() { var analysisEngine = Substitute.For(); - analysisEngine.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), Arg.Any()).Returns(Task.FromResult(Enumerable.Empty())); + analysisEngine.AnalyzeAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(Enumerable.Empty())); return analysisEngine; } diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs index 8fe89ec491..c5ee57e74a 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs @@ -26,7 +26,6 @@ using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; -using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http; @@ -197,7 +196,7 @@ public async Task StartListenAsync_AnalysisThrowsException_ReturnsInternalServer var exceptionMessage = "Simulated exception"; using var serverStarter2 = new HttpServerStarter(); serverStarter2.MockedRoslynAnalysisService - .When(x => x.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), Arg.Any())) + .When(x => x.AnalyzeAsync(Arg.Any(), Arg.Any())) .Do(_ => throw new InvalidOperationException(exceptionMessage)); serverStarter2.StartListeningOnBackgroundThread(); @@ -288,6 +287,6 @@ private static void MockServerSettings( private static void SimulateLongAnalysis(IRoslynAnalysisService roslynAnalysisService, int milliseconds) => roslynAnalysisService - .When(x => x.AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any>(), Arg.Any())) + .When(x => x.AnalyzeAsync(Arg.Any(), Arg.Any())) .Do(_ => Task.Delay(milliseconds).GetAwaiter().GetResult()); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs index 7d621a3455..c0bfd88146 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -35,6 +35,7 @@ private static readonly ImmutableDictionary { { Language.CSharp, new AnalyzerAssemblyContents() } }.ToImmutableDictionary(); private static readonly List DefaultActiveRules = new(); private static readonly Dictionary DefaultAnalysisProperties = new(); + private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); private ISonarLintXmlProvider sonarLintXmlProvider = null!; private IRoslynAnalyzerProvider roslynAnalyzerProvider = null!; @@ -47,7 +48,7 @@ public void TestInitialize() { sonarLintXmlProvider = Substitute.For(); roslynAnalyzerProvider = Substitute.For(); - roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies().Returns(DefaultAnalyzers); + roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto).Returns(DefaultAnalyzers); analyzerProfilesProvider = Substitute.For(); testLogger = Substitute.ForPartsOf(); @@ -101,7 +102,7 @@ public void GetConfiguration_CreatesConfigurationForEachLanguage() analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(roslynAnalysisProfiles); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Keys.Should().BeEquivalentTo(roslynAnalysisProfiles.Keys); foreach (var language in roslynAnalysisProfiles.Keys) @@ -132,7 +133,7 @@ public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(roslynAnalysisProfiles); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Should().BeEmpty(); testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoAnalyzers, language.Name)); @@ -156,7 +157,7 @@ public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(roslynAnalysisProfiles); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Should().BeEmpty(); testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoActiveRules, language.Name)); @@ -168,7 +169,7 @@ public void GetConfiguration_NoAnalysisProfiles_ReturnsEmptyDictionary() analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(new Dictionary()); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties); + var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Should().BeEmpty(); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs index 0376ec6fb3..fdf4e4565c 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs @@ -24,6 +24,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; @@ -33,6 +34,7 @@ public class RoslynAnalyzerProviderTests { private const string CsharpAnalyzerPath = "c:\\analyzers\\csharp.dll"; private const string VbAnalyzerPath = "c:\\analyzers\\vb.dll"; + private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); private IEmbeddedDotnetAnalyzersLocator analyzersLocator = null!; private IRoslynAnalyzerLoader roslynAnalyzerLoader = null!; @@ -58,9 +60,9 @@ public void MefCtor_CheckIsExported() => [TestMethod] public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary>()); + analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()).Returns(new Dictionary>()); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result.Should().BeEmpty(); } @@ -68,7 +70,7 @@ public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() [TestMethod] public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrectDictionary() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage() + analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()) .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("CS0001"); var vbAnalyzer = CreateAnalyzerWithDiagnostic("VB0001"); @@ -77,7 +79,7 @@ public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrect roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [csharpCodeFixer])); roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([vbAnalyzer], [vbCodeFixer])); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer); @@ -94,7 +96,7 @@ public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrect [TestMethod] public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage() + analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()) .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001", "SDUPLICATE"); var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "SDUPLICATE"); @@ -102,7 +104,7 @@ public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer1, csharpAnalyzer2], [])); roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([vbAnalyzer], [])); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result.Keys.Should().BeEquivalentTo(Language.CSharp, Language.VBNET); result[Language.CSharp].SupportedRuleKeys.Should().BeEquivalentTo("S001", "SDUPLICATE", "S002"); @@ -113,14 +115,15 @@ public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() public void GetAnalyzersByLanguage_MultipleAnalyzersPerLanguage_CombinesAllRules() { const string csharpAnalyzerPath2 = "c:\\analyzers\\csharp2.dll"; - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath, csharpAnalyzerPath2] } }); + analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()) + .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath, csharpAnalyzerPath2] } }); var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001"); var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "S003"); var csharpAnalyzer3 = CreateAnalyzerWithDiagnostic("S004"); roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer1], [])); roslynAnalyzerLoader.LoadAnalyzerAssembly(csharpAnalyzerPath2).Returns(new LoadedAnalyzerClasses([csharpAnalyzer2, csharpAnalyzer3], [])); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result.Keys.Should().BeEquivalentTo(Language.CSharp); result[Language.CSharp].Analyzers.Should().BeEquivalentTo(csharpAnalyzer1, csharpAnalyzer2, csharpAnalyzer3); @@ -130,11 +133,11 @@ public void GetAnalyzersByLanguage_MultipleAnalyzersPerLanguage_CombinesAllRules [TestMethod] public void GetAnalyzersByLanguage_NoCodeFixProviders_ReturnsEmptyMap() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); + analyzersLocator.GetAnalyzerFullPathsByLanguage(DefaultAnalyzerInfoDto).Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [])); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEmpty(); } @@ -142,13 +145,13 @@ public void GetAnalyzersByLanguage_NoCodeFixProviders_ReturnsEmptyMap() [TestMethod] public void GetAnalyzersByLanguage_CodeFixProviderWithMultipleDiagnostics_AddedToAllMappings() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); + analyzersLocator.GetAnalyzerFullPathsByLanguage(DefaultAnalyzerInfoDto).Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002", "S003"); var codeFixProvider = CreateCodeFixProviderWithDiagnostics("S001", "S002"); roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [codeFixProvider])); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( new Dictionary> { { "S001", [codeFixProvider] }, { "S002", [codeFixProvider] } }); @@ -157,14 +160,14 @@ public void GetAnalyzersByLanguage_CodeFixProviderWithMultipleDiagnostics_AddedT [TestMethod] public void GetAnalyzersByLanguage_MultipleCodeFixProvidersForSameId_AllAddedToSameCollection() { - analyzersLocator.GetBasicAnalyzerFullPathsByLanguage().Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); + analyzersLocator.GetAnalyzerFullPathsByLanguage(DefaultAnalyzerInfoDto).Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); var codeFixProvider1 = CreateCodeFixProviderWithDiagnostics("S001"); var codeFixProvider2 = CreateCodeFixProviderWithDiagnostics("S001"); roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [codeFixProvider1, codeFixProvider2])); - var result = testSubject.LoadAndProcessAnalyzerAssemblies(); + var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); result[Language.CSharp].CodeFixProvidersByRuleKey.Should().BeEquivalentTo( new Dictionary> { { "S001", [codeFixProvider1, codeFixProvider2] } }); diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs index bde7321b13..f930b72196 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -31,15 +31,16 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests; [TestClass] public class RoslynAnalysisServiceTests { - private static readonly List DefaultActiveRules = new() { new("sample-rule-id", new() { { "paramKey", "paramValue" } }) }; - private static readonly Dictionary DefaultAnalysisProperties = new() { { "sonar.cs.any", "any" }, }; + private static readonly List DefaultActiveRules = new() { new ActiveRuleDto("sample-rule-id", new Dictionary { { "paramKey", "paramValue" } }) }; + private static readonly Dictionary DefaultAnalysisProperties = new() { { "sonar.cs.any", "any" } }; private static readonly Dictionary DefaultAnalysisConfigurations = new() { { Language.CSharp, new RoslynAnalysisConfiguration() } }; - private static readonly List DefaultProjectAnalysisRequests = new() { new(Substitute.For(), []) }; - private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new("any", "any", new(1, 1, 1, 1))) }; + private static readonly List DefaultProjectAnalysisRequests = new() { new RoslynProjectAnalysisRequest(Substitute.For(), []) }; + private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new RoslynIssueLocation("any", "any", new RoslynIssueTextRange(1, 1, 1, 1))) }; + private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); + private IRoslynSolutionAnalysisCommandProvider analysisCommandProvider = null!; + private IRoslynAnalysisConfigurationProvider analysisConfigurationProvider = null!; private IRoslynAnalysisEngine analysisEngine = null!; - private IRoslynAnalysisConfigurationProvider analysisConfigurationProvider = null!; - private IRoslynSolutionAnalysisCommandProvider analysisCommandProvider = null!; private RoslynAnalysisService testSubject = null!; [TestInitialize] @@ -66,11 +67,15 @@ public void MefCtor_CheckIsExported() => public async Task AnalyzeAsync_PassesCorrectArgumentsToEngine() { string[] filePaths = [@"C:\file1.cs", @"C:\folder\file2.cs"]; - analysisConfigurationProvider.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties).Returns(DefaultAnalysisConfigurations); + analysisConfigurationProvider.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto).Returns(DefaultAnalysisConfigurations); analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(Arg.Is(x => x.SequenceEqual(filePaths))).Returns(DefaultProjectAnalysisRequests); analysisEngine.AnalyzeAsync(DefaultProjectAnalysisRequests, DefaultAnalysisConfigurations, Arg.Any()).Returns(DefaultIssues); + var analysisRequest = new AnalysisRequest + { + FileNames = filePaths.Select(x => new FileUri(x)).ToList(), ActiveRules = DefaultActiveRules, AnalysisProperties = DefaultAnalysisProperties, AnalyzerInfo = DefaultAnalyzerInfoDto + }; - var issues = await testSubject.AnalyzeAsync(filePaths.Select(x => new FileUri(x)).ToList(), DefaultActiveRules, DefaultAnalysisProperties, CancellationToken.None); + var issues = await testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); issues.Should().BeSameAs(DefaultIssues); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs index 80fcef0fc9..675a41229d 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs @@ -19,12 +19,11 @@ */ using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; public interface IEmbeddedDotnetAnalyzersLocator { - Dictionary> GetBasicAnalyzerFullPathsByLanguage(); - - Dictionary> GetEnterpriseAnalyzerFullPathsByLanguage(); + Dictionary> GetAnalyzerFullPathsByLanguage(AnalyzerInfoDto analyzerInfoDto); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs index bf4d616b74..7959b87772 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs @@ -25,5 +25,5 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalysisConfigurationProvider { - IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties); + IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties, AnalyzerInfoDto analyzerInfo); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs index 00113c3a70..687ec50159 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs @@ -22,12 +22,13 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalyzerProvider { - ImmutableDictionary LoadAndProcessAnalyzerAssemblies(); + ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo); } internal readonly record struct AnalyzerAssemblyContents( diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs index 120b6eb8b1..442cf42c02 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -36,11 +36,11 @@ internal class RoslynAnalysisConfigurationProvider( { private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); - public IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties) + public IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties, AnalyzerInfoDto analyzerInfo) { // todo add caching https://sonarsource.atlassian.net/browse/SLVS-2481 - var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(), activeRules, analysisProperties); + var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(analyzerInfo), activeRules, analysisProperties); var configurations = new Dictionary(); foreach (var analyzerAndLanguage in analysisProfilesByLanguage) diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs index b17ad10eaa..38d6333ea1 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs @@ -23,7 +23,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; @@ -32,9 +32,8 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [method: ImportingConstructor] internal class RoslynAnalyzerProvider(IEmbeddedDotnetAnalyzersLocator analyzersLocator, IRoslynAnalyzerLoader roslynAnalyzerLoader) : IRoslynAnalyzerProvider { - public ImmutableDictionary LoadAndProcessAnalyzerAssemblies() => - // todo SLVS-2410 Respect NET repackaging - LoadFromAssemblies(analyzersLocator.GetBasicAnalyzerFullPathsByLanguage()); + public ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo) => + LoadFromAssemblies(analyzersLocator.GetAnalyzerFullPathsByLanguage(analyzerInfo)); private ImmutableDictionary LoadFromAssemblies(Dictionary> analyzerFullPathsByLanguage) { diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs index 3454a47155..0e17d10035 100644 --- a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs @@ -27,4 +27,5 @@ public record AnalysisRequest public List FileNames { get; set; } = []; public List ActiveRules { get; set; } = []; public Dictionary AnalysisProperties { get; set; } = []; + public AnalyzerInfoDto AnalyzerInfo { get; set; } = null!; } diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs b/src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs new file mode 100644 index 0000000000..2096754313 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Models/AnalyzerInfoDto.cs @@ -0,0 +1,23 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record AnalyzerInfoDto(bool ShouldUseCsharpEnterprise, bool ShouldUseVbEnterprise); diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs index 29ec256cfc..d38331ff96 100644 --- a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -158,7 +158,7 @@ private async Task HandleRequest(IHttpListenerContext context, CancellationToken return; } - var issues = await roslynAnalysisService.AnalyzeAsync(analysisRequest.FileNames, analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, cancellationToken); + var issues = await roslynAnalysisService.AnalyzeAsync(analysisRequest, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); await httpRequestHandler.SendResponse(context, analysisRequestHandler.ParseAnalysisRequestResponse(issues.ToList())); } diff --git a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs index 4e2a33aa1b..6e076a54d3 100644 --- a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs @@ -20,10 +20,10 @@ using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; -using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + internal interface IRoslynAnalysisService { - Task> AnalyzeAsync(List files, List activeRules, Dictionary analysisProperties, CancellationToken cancellationToken); + Task> AnalyzeAsync(AnalysisRequest analysisRequest, CancellationToken cancellationToken); } diff --git a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs index 426ca33e5d..e3a4183a0a 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -22,22 +22,22 @@ using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; -using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer; [Export(typeof(IRoslynAnalysisService))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class RoslynAnalysisService(IRoslynAnalysisEngine analysisEngine, IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService +internal class RoslynAnalysisService( + IRoslynAnalysisEngine analysisEngine, + IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, + IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService { public Task> AnalyzeAsync( - List files, - List activeRules, - Dictionary analysisProperties, + AnalysisRequest analysisRequest, CancellationToken cancellationToken) => analysisEngine.AnalyzeAsync( - analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(files.Select(x => x.LocalPath).ToArray()), - analysisConfigurationProvider.GetConfiguration(activeRules, analysisProperties), + analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileNames.Select(x => x.LocalPath).ToArray()), + analysisConfigurationProvider.GetConfiguration(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), cancellationToken); } From ad8b403e7dab07bc3439eda10fef6929727a0a9c Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 29 Aug 2025 13:59:53 +0000 Subject: [PATCH 17/38] SLVS-2505 Make sure razor analysis for VB.NET files works (#6392) --- .../Analysis/Wrappers/RoslynProjectWrapper.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs index 4477de2036..263a030cce 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs @@ -29,14 +29,13 @@ internal class RoslynProjectWrapper(Project project) : IRoslynProjectWrapper { public string Name => project.Name; public bool SupportsCompilation => project.SupportsCompilation; - public AnalyzerOptions RoslynAnalyzerOptions => project.AnalyzerOptions; + public AnalyzerOptions RoslynAnalyzerOptions => project.AnalyzerOptions; - public async Task GetCompilationAsync(CancellationToken token) => - new RoslynCompilationWrapper((await project.GetCompilationAsync(token))!); + public async Task GetCompilationAsync(CancellationToken token) => new RoslynCompilationWrapper((await project.GetCompilationAsync(token))!); public bool ContainsDocument( string filePath, - [NotNullWhen(true)]out string? analysisFilePath) + [NotNullWhen(true)] out string? analysisFilePath) { analysisFilePath = project.Documents .Select(document => document.FilePath) @@ -47,7 +46,7 @@ public bool ContainsDocument( return analysisFilePath != null; } - // cshtml razor files are converted into .\file.cshtml..g.cs files when included in the compilation + // cshtml razor files are converted into .\file.cshtml..g.cs OR .\file.vbhtml..g.vb files when included in the compilation private static bool IsAssociatedGeneratedFile(string razorFilePath, string candidateDocumentPath) => - candidateDocumentPath.StartsWith(razorFilePath) && candidateDocumentPath.EndsWith(".g.cs"); + candidateDocumentPath.StartsWith(razorFilePath) && (candidateDocumentPath.EndsWith(".g.cs") || candidateDocumentPath.EndsWith(".g.vb")); } From 9bf76ae3031d9f508cfebec01ba322205b65d408 Mon Sep 17 00:00:00 2001 From: Vasileios Naskos <168648790+vnaskos-sonar@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:38:34 +0200 Subject: [PATCH 18/38] SLVS-2403 Configure CI/CD pipeline (#6394) --- build/CopyDependencies/CopyDependencies.targets | 3 +-- build/DownloadDependencies/CommonProperties.props | 3 +-- build/DownloadDependencies/JarProcessing.targets | 9 +-------- src/EmbeddedSonarAnalyzer.props | 2 +- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/build/CopyDependencies/CopyDependencies.targets b/build/CopyDependencies/CopyDependencies.targets index f737b5d69c..167acdec68 100644 --- a/build/CopyDependencies/CopyDependencies.targets +++ b/build/CopyDependencies/CopyDependencies.targets @@ -37,8 +37,7 @@ - - + diff --git a/build/DownloadDependencies/CommonProperties.props b/build/DownloadDependencies/CommonProperties.props index ee7514765e..ed8603c2eb 100644 --- a/build/DownloadDependencies/CommonProperties.props +++ b/build/DownloadDependencies/CommonProperties.props @@ -62,8 +62,7 @@ sonarqube-ide-visualstudio-roslyn-plugin-$(EmbeddedSonarSqvsRoslynJarVersion).jar $(JarDownloadDir)\$(SonarSqvsRoslynPluginFileName) - - https://repox.jfrog.io/pathToPlugin/$(EmbeddedSonarSqvsRoslynJarVersion)/$(SonarSqvsRoslynPluginFileName) + https://repox.jfrog.io/artifactory/sonarsource/org/sonarsource/sonarlint/visualstudio/roslyn/sonarqube-ide-visualstudio-roslyn-plugin/$(EmbeddedSonarSqvsRoslynJarVersion)/$(SonarSqvsRoslynPluginFileName) diff --git a/build/DownloadDependencies/JarProcessing.targets b/build/DownloadDependencies/JarProcessing.targets index 6f44af6d09..efc113cce7 100644 --- a/build/DownloadDependencies/JarProcessing.targets +++ b/build/DownloadDependencies/JarProcessing.targets @@ -54,14 +54,7 @@ - - - - - - - + diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index c389246465..8a933ce868 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0.0 + 1.0.0.11 10.34.0.83431 1.0.0 From c4a24d0bbd87d36bd9b0d4dcdcbfc97e7b87c7cd Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:05:47 +0200 Subject: [PATCH 19/38] SLVS-2506 Make quick fix application logic abstract to allow for different quickfix types (#6393) [SLVS-2506](https://sonarsource.atlassian.net/browse/SLVS-2506) Part of SLVS-2406 [SLVS-2506]: https://sonarsource.atlassian.net/browse/SLVS-2506?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- src/Core/Analysis/AnalysisIssue.cs | 8 +- src/Core/Analysis/IAnalysisIssue.cs | 2 +- src/Core/Analysis/IQuickFix.cs | 6 +- .../{QuickFix.cs => TextBasedQuickFix.cs} | 4 +- .../SpanTranslatorTests.cs | 31 ++ src/Infrastructure.VS/SpanTranslator.cs | 5 + ...nalysisIssueVisualizationConverterTests.cs | 73 +++-- .../QuickFixes/QuickFixActionsSourceTests.cs | 12 +- .../QuickFixSuggestedActionTests.cs | 304 ++++-------------- .../Models/QuickFixVisualizationTests.cs | 12 +- .../TextBasedQuickFixApplicationTests.cs | 139 ++++++++ .../QuickFixes/QuickFixActionsSource.cs | 2 +- .../QuickFixes/QuickFixSuggestedAction.cs | 62 +--- .../Models/AnalysisIssueVisualization.cs | 6 +- .../AnalysisIssueVisualizationConverter.cs | 51 +-- src/IssueViz/Models/IQuickFixApplication.cs | 30 ++ ....cs => ITextBasedQuickFixVisualization.cs} | 45 +-- .../Models/TextBasedQuickFixApplication.cs | 52 +++ ...iseFindingToAnalysisIssueConverterTests.cs | 17 +- .../RaiseFindingToAnalysisIssueConverter.cs | 14 +- src/TestInfrastructure/DummyAnalysisIssue.cs | 2 +- 21 files changed, 464 insertions(+), 413 deletions(-) rename src/Core/Analysis/{QuickFix.cs => TextBasedQuickFix.cs} (92%) create mode 100644 src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs create mode 100644 src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs create mode 100644 src/IssueViz/Models/IQuickFixApplication.cs rename src/IssueViz/Models/{QuickFixVisualization.cs => ITextBasedQuickFixVisualization.cs} (60%) create mode 100644 src/IssueViz/Models/TextBasedQuickFixApplication.cs diff --git a/src/Core/Analysis/AnalysisIssue.cs b/src/Core/Analysis/AnalysisIssue.cs index 9f4a9718a0..5f885c058b 100644 --- a/src/Core/Analysis/AnalysisIssue.cs +++ b/src/Core/Analysis/AnalysisIssue.cs @@ -23,7 +23,7 @@ namespace SonarLint.VisualStudio.Core.Analysis public class AnalysisIssue : IAnalysisIssue { private static readonly IReadOnlyList EmptyFlows = []; - private static readonly IReadOnlyList EmptyFixes = []; + private static readonly IReadOnlyList EmptyFixes = []; public AnalysisIssue( Guid? id, @@ -35,7 +35,7 @@ public AnalysisIssue( Impact highestImpact, IAnalysisIssueLocation primaryLocation, IReadOnlyList flows, - IReadOnlyList fixes = null) + IReadOnlyList fixes = null) { Id = id; RuleKey = ruleKey; @@ -63,7 +63,7 @@ public AnalysisIssue( public bool IsResolved { get; } public string IssueServerKey { get; } - public IReadOnlyList Fixes { get; } + public IReadOnlyList Fixes { get; } public Impact HighestImpact { get; } } @@ -80,7 +80,7 @@ public AnalysisHotspotIssue( IAnalysisIssueLocation primaryLocation, IReadOnlyList flows, HotspotStatus hotspotStatus, - IReadOnlyList fixes = null, + IReadOnlyList fixes = null, HotspotPriority? hotspotPriority = null) : base(id, ruleKey, issueServerKey, isResolved, severity, type, highestImpact, primaryLocation, flows, fixes) { diff --git a/src/Core/Analysis/IAnalysisIssue.cs b/src/Core/Analysis/IAnalysisIssue.cs index 966c6f44c5..61879585b3 100644 --- a/src/Core/Analysis/IAnalysisIssue.cs +++ b/src/Core/Analysis/IAnalysisIssue.cs @@ -26,7 +26,7 @@ public interface IAnalysisIssue : IAnalysisIssueBase AnalysisIssueType? Type { get; } - IReadOnlyList Fixes { get; } + IReadOnlyList Fixes { get; } Impact HighestImpact { get; } } diff --git a/src/Core/Analysis/IQuickFix.cs b/src/Core/Analysis/IQuickFix.cs index 4f587ed089..732aa878f6 100644 --- a/src/Core/Analysis/IQuickFix.cs +++ b/src/Core/Analysis/IQuickFix.cs @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Generic; - namespace SonarLint.VisualStudio.Core.Analysis { - public interface IQuickFix + public interface IQuickFixBase; + + public interface ITextBasedQuickFix : IQuickFixBase { string Message { get; } IReadOnlyList Edits { get; } diff --git a/src/Core/Analysis/QuickFix.cs b/src/Core/Analysis/TextBasedQuickFix.cs similarity index 92% rename from src/Core/Analysis/QuickFix.cs rename to src/Core/Analysis/TextBasedQuickFix.cs index 8fbdffe546..22a7267fe2 100644 --- a/src/Core/Analysis/QuickFix.cs +++ b/src/Core/Analysis/TextBasedQuickFix.cs @@ -23,9 +23,9 @@ namespace SonarLint.VisualStudio.Core.Analysis { - public class QuickFix : IQuickFix + public class TextBasedQuickFix : ITextBasedQuickFix { - public QuickFix(string message, IReadOnlyList edits) + public TextBasedQuickFix(string message, IReadOnlyList edits) { if (edits == null || edits.Count == 0) { diff --git a/src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs b/src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs new file mode 100644 index 0000000000..acd6f4d87f --- /dev/null +++ b/src/Infrastructure.VS.UnitTests/SpanTranslatorTests.cs @@ -0,0 +1,31 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests; + +[TestClass] +public class SpanTranslatorTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); +} diff --git a/src/Infrastructure.VS/SpanTranslator.cs b/src/Infrastructure.VS/SpanTranslator.cs index 3305e663e7..7698724837 100644 --- a/src/Infrastructure.VS/SpanTranslator.cs +++ b/src/Infrastructure.VS/SpanTranslator.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; using Microsoft.VisualStudio.Text; namespace SonarLint.VisualStudio.Infrastructure.VS @@ -30,8 +32,11 @@ public interface ISpanTranslator SnapshotSpan TranslateTo(SnapshotSpan original, ITextSnapshot targetSnapshot, SpanTrackingMode spanTrackingMode); } + [Export(typeof(ISpanTranslator))] + [PartCreationPolicy(CreationPolicy.Shared)] public class SpanTranslator : ISpanTranslator { + [ExcludeFromCodeCoverage] public SnapshotSpan TranslateTo(SnapshotSpan original, ITextSnapshot targetSnapshot, SpanTrackingMode spanTrackingMode) => diff --git a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs index a7bfe7489a..ed0bed87e3 100644 --- a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs +++ b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs @@ -22,8 +22,10 @@ using Microsoft.VisualStudio.Text; using Moq; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Editor; using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.IssueVisualization.UnitTests; @@ -41,9 +43,18 @@ public void TestInitialize() issueSpanCalculatorMock = new Mock(); textSnapshotMock = Mock.Of(); - testSubject = new AnalysisIssueVisualizationConverter(issueSpanCalculatorMock.Object); + testSubject = new AnalysisIssueVisualizationConverter(issueSpanCalculatorMock.Object, Substitute.For()); } + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] public void Convert_NoTextBuffer_IssueWithNullSpan() { @@ -73,7 +84,7 @@ public void Convert_NoTextBuffer_SecondaryLocationSpansAreNotCalculated() [TestMethod] public void Convert_NoTextBuffer_QuickFixSpansAreNotCalculated() { - var issue = CreateIssue(Mock.Of()); + var issue = CreateIssue(Mock.Of()); var result = testSubject.Convert(issue, textSnapshot: null); @@ -110,8 +121,8 @@ public void Convert_IssueHasQuickFixes_QuickFixesSpansAreCalculated() SetupSpanCalculator(textRange3, span3); var edit3 = CreateEdit(textRange3); - var fix1 = new QuickFix("fix1", new[] { edit1, edit2 }); - var fix2 = new QuickFix("fix2", new[] { edit3 }); + var fix1 = new TextBasedQuickFix("fix1", [edit1, edit2]); + var fix2 = new TextBasedQuickFix("fix2", [edit3]); var issue = CreateIssue(fix1, fix2); var result = testSubject.Convert(issue, textSnapshotMock); @@ -119,19 +130,21 @@ public void Convert_IssueHasQuickFixes_QuickFixesSpansAreCalculated() result.QuickFixes.Should().NotBeEmpty(); result.QuickFixes.Count.Should().Be(2); - result.QuickFixes[0].Fix.Should().Be(fix1); - result.QuickFixes[0].EditVisualizations.Should().NotBeEmpty(); - result.QuickFixes[0].EditVisualizations.Count.Should().Be(2); - result.QuickFixes[0].EditVisualizations[0].Edit.Should().Be(edit1); - result.QuickFixes[0].EditVisualizations[0].Span.Should().Be(span1); - result.QuickFixes[0].EditVisualizations[1].Edit.Should().Be(edit2); - result.QuickFixes[0].EditVisualizations[1].Span.Should().Be(span2); - - result.QuickFixes[1].Fix.Should().Be(fix2); - result.QuickFixes[1].EditVisualizations.Should().NotBeEmpty(); - result.QuickFixes[1].EditVisualizations.Count.Should().Be(1); - result.QuickFixes[1].EditVisualizations[0].Edit.Should().Be(edit3); - result.QuickFixes[1].EditVisualizations[0].Span.Should().Be(span3); + var quickFix1 = result.QuickFixes[0].Should().BeOfType().Subject.QuickFixVisualization; + quickFix1.Fix.Should().Be(fix1); + quickFix1.EditVisualizations.Should().NotBeEmpty(); + quickFix1.EditVisualizations.Count.Should().Be(2); + quickFix1.EditVisualizations[0].Edit.Should().Be(edit1); + quickFix1.EditVisualizations[0].Span.Should().Be(span1); + quickFix1.EditVisualizations[1].Edit.Should().Be(edit2); + quickFix1.EditVisualizations[1].Span.Should().Be(span2); + + var quickFix2 = result.QuickFixes[1].Should().BeOfType().Subject.QuickFixVisualization; + quickFix2.Fix.Should().Be(fix2); + quickFix2.EditVisualizations.Should().NotBeEmpty(); + quickFix2.EditVisualizations.Count.Should().Be(1); + quickFix2.EditVisualizations[0].Edit.Should().Be(edit3); + quickFix2.EditVisualizations[0].Span.Should().Be(span3); } [TestMethod] @@ -156,10 +169,10 @@ public void Convert_IssueHasNoFlows_IssueVisualizationWithoutFlows() SetupSpanCalculator(issue.PrimaryLocation.TextRange, issueSpan); var expectedIssueVisualization = new AnalysisIssueVisualization( - Array.Empty(), + [], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -177,13 +190,13 @@ public void Convert_IssueHasOneFlow_IssueVisualizationWithFlow() SetupSpanCalculator(issue.PrimaryLocation.TextRange, issueSpan); var expectedLocationVisualization = new AnalysisIssueLocationVisualization(1, location); - var expectedFlowVisualization = new AnalysisIssueFlowVisualization(1, new[] { expectedLocationVisualization }, flow); + var expectedFlowVisualization = new AnalysisIssueFlowVisualization(1, [expectedLocationVisualization], flow); var expectedIssueVisualization = new AnalysisIssueVisualization( - new[] { expectedFlowVisualization }, + [expectedFlowVisualization], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -207,18 +220,18 @@ public void Convert_IssueHasTwoFlows_IssueVisualizationWithTwoFlows() var expectedFirstFlowFirstLocationVisualization = new AnalysisIssueLocationVisualization(1, firstFlowFirstLocation); var expectedFirstFlowSecondLocationVisualization = new AnalysisIssueLocationVisualization(2, firstFlowSecondLocation); - var expectedFirstFlowVisualization = new AnalysisIssueFlowVisualization(1, new[] { expectedFirstFlowFirstLocationVisualization, expectedFirstFlowSecondLocationVisualization }, firstFlow); + var expectedFirstFlowVisualization = new AnalysisIssueFlowVisualization(1, [expectedFirstFlowFirstLocationVisualization, expectedFirstFlowSecondLocationVisualization], firstFlow); var expectedSecondFlowFirstLocationVisualization = new AnalysisIssueLocationVisualization(1, secondFlowFirstLocation); var expectedSecondFlowSecondLocationVisualization = new AnalysisIssueLocationVisualization(2, secondFlowSecondLocation); var expectedSecondFlowVisualization - = new AnalysisIssueFlowVisualization(2, new[] { expectedSecondFlowFirstLocationVisualization, expectedSecondFlowSecondLocationVisualization }, secondFlow); + = new AnalysisIssueFlowVisualization(2, [expectedSecondFlowFirstLocationVisualization, expectedSecondFlowSecondLocationVisualization], secondFlow); var expectedIssueVisualization = new AnalysisIssueVisualization( - new[] { expectedFirstFlowVisualization, expectedSecondFlowVisualization }, + [expectedFirstFlowVisualization, expectedSecondFlowVisualization], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -243,13 +256,13 @@ public void Convert_IssueHasLocationsInDifferentFiles_CalculatesSpanForLocations var expectedLocation1 = new AnalysisIssueLocationVisualization(1, locationInSameFile) { Span = locationSpan }; var expectedLocation2 = new AnalysisIssueLocationVisualization(2, locationInAnotherFile) { Span = null }; - var expectedFlow = new AnalysisIssueFlowVisualization(1, new[] { expectedLocation1, expectedLocation2 }, flow); + var expectedFlow = new AnalysisIssueFlowVisualization(1, [expectedLocation1, expectedLocation2], flow); var expectedIssueVisualization = new AnalysisIssueVisualization( - new[] { expectedFlow }, + [expectedFlow], issue, issueSpan, - Array.Empty()); + []); var actualIssueVisualization = testSubject.Convert(issue, textSnapshotMock); @@ -276,7 +289,7 @@ private void AssertConversion(IAnalysisIssueVisualization expectedIssueVisualiza actualIssueVisualization.Flows.Should().BeEquivalentTo(expectedIssueVisualization.Flows, c => c.WithStrictOrdering()); } - private IAnalysisIssue CreateIssue(params IQuickFix[] quickFixes) + private IAnalysisIssue CreateIssue(params IQuickFixBase[] quickFixes) { var issue = new AnalysisIssue( Guid.NewGuid(), diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs index 2d35cac047..7d7cf75abb 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs @@ -366,7 +366,7 @@ private QuickFixActionsSource CreateTestSubject( threadHandling); } - private IAnalysisIssueVisualization CreateIssueViz(params IQuickFixVisualization[] fixes) + private IAnalysisIssueVisualization CreateIssueViz(params IQuickFixApplication[] fixes) { var issueViz = new Mock(); issueViz.Setup(x => x.QuickFixes).Returns(fixes); @@ -387,13 +387,13 @@ private Mock> CreateTagAggregatorForIssues(IAn return issueLocationsTagAggregator; } - private IQuickFixVisualization CreateQuickFixViz(bool canBeApplied, string message = null) + private IQuickFixApplication CreateQuickFixViz(bool canBeApplied, string message = null) { - var quickFixViz = new Mock(); - quickFixViz.Setup(x => x.CanBeApplied(textBuffer.CurrentSnapshot)).Returns(canBeApplied); - quickFixViz.Setup(x => x.Fix.Message).Returns(message); + var quickFixApplication = Substitute.For(); + quickFixApplication.Message.Returns(message); + quickFixApplication.CanBeApplied(textBuffer.CurrentSnapshot).Returns(canBeApplied); - return quickFixViz.Object; + return quickFixApplication; } private static Mock> CreateThrowingAggregator(Exception ex) diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs index 2665025ee8..0beff5619a 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs @@ -19,295 +19,115 @@ */ using Microsoft.VisualStudio.Text; -using Moq; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; -using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes; using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.TestInfrastructure; namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Editor.QuickActions.QuickFixes { [TestClass] public class QuickFixSuggestedActionTests { - [TestMethod] - public void DisplayName_ReturnsFixMessage() - { - var quickFixViz = new Mock(); - quickFixViz.Setup(x => x.Fix.Message).Returns("some fix"); - - var testSubject = CreateTestSubject(quickFixViz.Object); - - testSubject.DisplayText.Should().Be(Resources.ProductNameCommandPrefix + "some fix"); - } - - [TestMethod] - public void Invoke_QuickFixCanBeApplied_TelemetryIsSent() - { - var snapshot = CreateTextSnapshot(); - var quickFixViz = CreateQuickFixViz(snapshot.Object); - var textBuffer = CreateTextBuffer(snapshot.Object); - - var issueViz = new Mock(); - issueViz.Setup(x => x.RuleId).Returns("some rule"); - - var telemetryManager = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, - textBuffer.Object, - issueViz: issueViz.Object, - telemetryManager: telemetryManager.Object); - - testSubject.Invoke(CancellationToken.None); - - telemetryManager.Verify(x => x.QuickFixApplied("some rule"), Times.Once); - telemetryManager.VerifyNoOtherCalls(); - } - - [TestMethod] - public void Invoke_QuickFixCannotBeApplied_TelemetryNotSent() + private IQuickFixApplication quickFixApplication; + private ITextBuffer textBuffer; + private IAnalysisIssueVisualization issueViz; + private IQuickFixesTelemetryManager telemetryManager; + private TestLogger logger; + private NoOpThreadHandler threadHandling; + private ITextSnapshot snapshot; + private QuickFixSuggestedAction testSubject; + private const string RuleId = "test-rule-id"; + + [TestInitialize] + public void TestInitialize() { - var snapshot = CreateTextSnapshot(); - var quickFixViz = CreateNonApplicableQuickFixViz(snapshot.Object); - var textBuffer = CreateTextBuffer(snapshot.Object); - - var telemetryManager = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, telemetryManager: telemetryManager.Object); - testSubject.Invoke(CancellationToken.None); - - telemetryManager.Verify(x => x.QuickFixApplied(It.IsAny()), Times.Never); - telemetryManager.VerifyNoOtherCalls(); + quickFixApplication = Substitute.For(); + snapshot = CreateTextSnapshot(); + textBuffer = CreateTextBuffer(snapshot); + issueViz = Substitute.For(); + issueViz.RuleId.Returns(RuleId); + telemetryManager = Substitute.For(); + logger = Substitute.ForPartsOf(); + threadHandling = Substitute.ForPartsOf(); + + testSubject = new QuickFixSuggestedAction( + quickFixApplication, + textBuffer, + issueViz, + telemetryManager, + logger, + threadHandling); } [TestMethod] - public void Invoke_AppliesFixWithOneEdit() + public void DisplayName_ReturnsFixMessage() { - var snapshot = CreateTextSnapshot(); - - var span = new Span(1, 10); - var editVisualization = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span)); - var quickFixViz = CreateQuickFixViz(snapshot.Object, editVisualization.Object); - var textEdit = new Mock(MockBehavior.Strict); - var textBuffer = CreateTextBuffer(snapshot.Object, textEdit.Object); - - var sequence = new MockSequence(); + const string message = "some fix"; + quickFixApplication.Message.Returns(message); - textBuffer.InSequence(sequence).Setup(t => t.CreateEdit()).Returns(textEdit.Object); - textEdit.InSequence(sequence).Setup(t => t.Replace(span, "edit")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Apply()).Returns(Mock.Of()); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object); - testSubject.Invoke(CancellationToken.None); - - textBuffer.Verify(tb => tb.CreateEdit(), Times.Once(), "CreateEdit should be called once"); - textEdit.Verify(tb => tb.Replace(It.IsAny(), It.IsAny()), Times.Exactly(1), "Replace should be called one time"); - textEdit.Verify(tb => tb.Apply(), Times.Once(), "Apply should be called once"); + testSubject.DisplayText.Should().Be(Resources.ProductNameCommandPrefix + message); } [TestMethod] - public void Invoke_AppliesFixWithMultipleEdits() + public void Invoke_AppliesFixOnUiThreadWithTelemetry() { - var snapshot = CreateTextSnapshot(); - - var span1 = new Span(1, 10); - var editVisualization1 = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span1), "edit1"); - - var span2 = new Span(2, 20); - var editVisualization2 = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span2), "edit2"); + ConfigureQuickFixApplication(); - var span3 = new Span(3, 30); - var editVisualization3 = CreateEditVisualization(new SnapshotSpan(snapshot.Object, span3), "edit3"); - - var quickFixViz = CreateQuickFixViz(snapshot.Object, - editVisualization1.Object, - editVisualization2.Object, - editVisualization3.Object); - - var textEdit = new Mock(MockBehavior.Strict); - var textBuffer = CreateTextBuffer(snapshot.Object, textEdit.Object); - - var sequence = new MockSequence(); - - textBuffer.InSequence(sequence).Setup(t => t.CreateEdit()).Returns(textEdit.Object); - textEdit.InSequence(sequence).Setup(t => t.Replace(span1, "edit1")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Replace(span2, "edit2")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Replace(span3, "edit3")).Returns(true); - textEdit.InSequence(sequence).Setup(t => t.Apply()).Returns(Mock.Of()); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object); testSubject.Invoke(CancellationToken.None); - textBuffer.Verify(tb => tb.CreateEdit(), Times.Once(), "CreateEdit should be called once"); - textEdit.Verify(tb => tb.Replace(It.IsAny(), It.IsAny()), Times.Exactly(3), "Replace should be called three time"); - textEdit.Verify(tb => tb.Apply(), Times.Once(), "Apply should be called once"); + Received.InOrder(() => + { + quickFixApplication.CanBeApplied(snapshot); + threadHandling.Run(Arg.Any>>()); + threadHandling.SwitchToMainThreadAsync(); + quickFixApplication.ApplyAsync(snapshot, issueViz, Arg.Any()); + telemetryManager.QuickFixApplied(RuleId); + }); } [TestMethod] public void Invoke_CancellationTokenIsCancelled_NoChanges() { - var quickFixViz = new Mock(); - var textBuffer = new Mock(); - var issueViz = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, issueViz: issueViz.Object); - testSubject.Invoke(new CancellationToken(canceled: true)); - quickFixViz.VerifyNoOtherCalls(); - textBuffer.VerifyNoOtherCalls(); - issueViz.VerifyNoOtherCalls(); + quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default, default); + issueViz.DidNotReceiveWithAnyArgs().Span = Arg.Any(); + telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); } [TestMethod] public void Invoke_QuickFixIsNotApplicable_NoChanges() { - var snapshot = Mock.Of(); - var quickFixViz = CreateNonApplicableQuickFixViz(snapshot); - var issueViz = new Mock(); - var textBuffer = CreateTextBuffer(snapshot); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, issueViz: issueViz.Object); + ConfigureNonApplicableQuickFixApplication(); testSubject.Invoke(CancellationToken.None); - quickFixViz.Verify(x => x.CanBeApplied(snapshot), Times.Once); - quickFixViz.VerifyNoOtherCalls(); - - textBuffer.VerifyGet(x => x.CurrentSnapshot, Times.Once); - textBuffer.VerifyNoOtherCalls(); - - issueViz.VerifyGet(x => x.RuleId, Times.Once); - issueViz.VerifyNoOtherCalls(); + quickFixApplication.Received(1).CanBeApplied(snapshot); + quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default, default); + issueViz.DidNotReceiveWithAnyArgs().Span = Arg.Any(); + telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); } - [TestMethod] - public void Invoke_SpansAreTranslatedCorrectly() - { - var snapshot = CreateTextSnapshot(); - var span1 = new Span(1, 10); - var span2 = new Span(20, 30); - - var originalSnapshotSpan = new SnapshotSpan(snapshot.Object, span1); - var modifiedSnapshotSpan = new SnapshotSpan(snapshot.Object, span2); - - var spanTranslator = new Mock(); - spanTranslator - .Setup(x => x.TranslateTo(originalSnapshotSpan, snapshot.Object, SpanTrackingMode.EdgeExclusive)) - .Returns(modifiedSnapshotSpan); - - var editVisualization = CreateEditVisualization(originalSnapshotSpan, text: "some edit"); - var quickFixViz = CreateQuickFixViz(snapshot.Object, editVisualization.Object); - - var textEdit = new Mock(); - var textBuffer = CreateTextBuffer(snapshot.Object, textEdit.Object); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, spanTranslator.Object); - testSubject.Invoke(CancellationToken.None); - - textEdit.Verify(t => t.Replace(span2, "some edit"), Times.Once); - } - - [TestMethod] - public void Invoke_SpanInvalidatedCorrectly() + private static ITextSnapshot CreateTextSnapshot() { - var snapshot = CreateTextSnapshot(); - var editVisualization = CreateEditVisualization(new SnapshotSpan(snapshot.Object, new Span(1, 10))); - var quickFixViz = CreateQuickFixViz(snapshot.Object, editVisualization.Object); - var textBuffer = CreateTextBuffer(snapshot.Object); - - var issueViz = new Mock(); - - var testSubject = CreateTestSubject(quickFixViz.Object, textBuffer.Object, issueViz: issueViz.Object); - testSubject.Invoke(CancellationToken.None); - - issueViz.VerifySet(iv => iv.Span = new SnapshotSpan()); + var snapshot = Substitute.For(); + snapshot.Length.Returns(int.MaxValue); + return snapshot; } - private static Mock CreateTextSnapshot() - { - var snapShot = new Mock(); - snapShot.SetupGet(ss => ss.Length).Returns(int.MaxValue); - - return snapShot; - } - - private static QuickFixSuggestedAction CreateTestSubject( - IQuickFixVisualization quickFixViz, - ITextBuffer textBuffer = null, - ISpanTranslator spanTranslator = null, - IAnalysisIssueVisualization issueViz = null, - IQuickFixesTelemetryManager telemetryManager = null) - { - if (spanTranslator == null) - { - SnapshotSpan originalSnapshotSpan = new(); + private void ConfigureQuickFixApplication() => ConfigureQuickFixApplication(true); - var doNothingSpanTranslator = new Mock(); - doNothingSpanTranslator.Setup(x => x.TranslateTo( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Callback((SnapshotSpan snapshotSpan, ITextSnapshot textSnapshot, SpanTrackingMode mode) - => originalSnapshotSpan = snapshotSpan) - .Returns(() => originalSnapshotSpan); + private void ConfigureNonApplicableQuickFixApplication() => ConfigureQuickFixApplication(false); - spanTranslator = doNothingSpanTranslator.Object; - } - - issueViz ??= Mock.Of(); - telemetryManager ??= Mock.Of(); - - return new QuickFixSuggestedAction(quickFixViz, - textBuffer, - issueViz, - telemetryManager, - Mock.Of(), - spanTranslator); - } - - private static Mock CreateQuickFixViz(ITextSnapshot snapShot, params IQuickFixEditVisualization[] editVisualizations) => - CreateQuickFixViz(snapShot, true, editVisualizations); - - private static Mock CreateNonApplicableQuickFixViz(ITextSnapshot snapShot) => CreateQuickFixViz(snapShot, false); - - private static Mock CreateQuickFixViz( - ITextSnapshot snapShot, - bool canBeApplied = true, - params IQuickFixEditVisualization[] editVisualizations) - { - var quickFixViz = new Mock(); - - quickFixViz - .Setup(x => x.EditVisualizations) - .Returns(editVisualizations); - - quickFixViz - .Setup(x => x.CanBeApplied(snapShot)) + private void ConfigureQuickFixApplication(bool canBeApplied) => + quickFixApplication.CanBeApplied(snapshot) .Returns(canBeApplied); - return quickFixViz; - } - - private static Mock CreateEditVisualization(SnapshotSpan snapshotSpan, string text = "edit") - { - var editVisualization = new Mock(); - - editVisualization.Setup(e => e.Edit.NewText).Returns(text); - editVisualization.Setup(e => e.Span).Returns(snapshotSpan); - - return editVisualization; - } - - private static Mock CreateTextBuffer(ITextSnapshot snapShot, ITextEdit textEdit = null) + private static ITextBuffer CreateTextBuffer(ITextSnapshot snapShot) { - textEdit ??= Mock.Of(); - - var textBuffer = new Mock(MockBehavior.Strict); - textBuffer.Setup(x => x.CurrentSnapshot).Returns(snapShot); - textBuffer.Setup(t => t.CreateEdit()).Returns(textEdit); - + var textBuffer = Substitute.For(); + textBuffer.CurrentSnapshot.Returns(snapShot); return textBuffer; } } diff --git a/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs b/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs index 84e157923b..27aaa9f71a 100644 --- a/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs +++ b/src/IssueViz.UnitTests/Models/QuickFixVisualizationTests.cs @@ -95,13 +95,13 @@ public void CanBeApplied_SameTextButDifferentCasing_False() testSubject.CanBeApplied(snapshot.Object).Should().BeFalse(); } - private IQuickFixEditVisualization SetupEditVisualization(Mock snapshot, + private ITextBasedQuickFixEditVisualization SetupEditVisualization(Mock snapshot, Mock spanTranslator, string originalText, string updatedText) => SetupEditVisualization(snapshot, spanTranslator, new Span(1, 2), originalText, updatedText); - private IQuickFixEditVisualization SetupEditVisualization(Mock snapshot, + private ITextBasedQuickFixEditVisualization SetupEditVisualization(Mock snapshot, Mock spanTranslator, Span originalSpan, bool isSameText) @@ -112,7 +112,7 @@ private IQuickFixEditVisualization SetupEditVisualization(Mock sn return SetupEditVisualization(snapshot, spanTranslator, originalSpan, originalText, updatedText); } - private IQuickFixEditVisualization SetupEditVisualization(Mock snapshot, + private ITextBasedQuickFixEditVisualization SetupEditVisualization(Mock snapshot, Mock spanTranslator, Span originalSpan, string originalText, @@ -120,7 +120,7 @@ private IQuickFixEditVisualization SetupEditVisualization(Mock sn { var originalSnapshotSpan = new SnapshotSpan(snapshot.Object, originalSpan); - var editViz = new Mock(); + var editViz = new Mock(); editViz.Setup(x => x.Span).Returns(originalSnapshotSpan); var updatedSnapshotSpan = new SnapshotSpan(snapshot.Object, new Span(originalSpan.Start + 1, originalSpan.Length)); @@ -143,7 +143,7 @@ private static Mock CreateSnapshot() return snapshot; } - private IQuickFixVisualization CreateTestSubject(ISpanTranslator spanTranslator, params IQuickFixEditVisualization[] editVisualizations) => - new QuickFixVisualization(Mock.Of(), editVisualizations, spanTranslator); + private ITextBasedQuickFixVisualization CreateTestSubject(ISpanTranslator spanTranslator, params ITextBasedQuickFixEditVisualization[] editVisualizations) => + new TextBasedQuickFixVisualization(Mock.Of(), editVisualizations, spanTranslator); } } diff --git a/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs b/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs new file mode 100644 index 0000000000..42abb5c439 --- /dev/null +++ b/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs @@ -0,0 +1,139 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Text; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.IssueVisualization.Models; + +namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.Models; + +[TestClass] +public class TextBasedQuickFixApplicationTests +{ + private ITextSnapshot snapshot; + private ITextBasedQuickFixVisualization quickFixVisualization; + private ISpanTranslator spanTranslator; + private ITextBuffer textBuffer; + private ITextEdit textEdit; + private IAnalysisIssueVisualization issueViz; + private TextBasedQuickFixApplication testSubject; + + [TestInitialize] + public void TestInitialize() + { + snapshot = Substitute.For(); + snapshot.Length.Returns(int.MaxValue); + quickFixVisualization = Substitute.For(); + spanTranslator = Substitute.For(); + issueViz = Substitute.For(); + + testSubject = new TextBasedQuickFixApplication(quickFixVisualization, spanTranslator); + + SetupTextBufferAndEdit(); + } + + [TestMethod] + public void Message_ReturnsFixMessage() + { + const string expectedMessage = "test message"; + var quickFix = Substitute.For(); + quickFix.Message.Returns(expectedMessage); + quickFixVisualization.Fix.Returns(quickFix); + + testSubject.Message.Should().Be(expectedMessage); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void CanBeApplied_DelegatesToVisualization(bool expectedResult) + { + quickFixVisualization.CanBeApplied(snapshot).Returns(expectedResult); + + testSubject.CanBeApplied(snapshot).Should().Be(expectedResult); + quickFixVisualization.Received(1).CanBeApplied(snapshot); + } + + [TestMethod] + public async Task ApplyAsync_CreatesEditAndAppliesAllChanges() + { + var span1 = new SnapshotSpan(snapshot, new Span(1, 10)); + var span2 = new SnapshotSpan(snapshot, new Span(20, 5)); + var translatedSpan1 = new SnapshotSpan(snapshot, new Span(2, 10)); + var translatedSpan2 = new SnapshotSpan(snapshot, new Span(25, 5)); + SetupEditVisualizations( + (span1, "new text 1", translatedSpan1), + (span2, "new text 2", translatedSpan2)); + + await testSubject.ApplyAsync(snapshot, issueViz, CancellationToken.None); + + textBuffer.Received(1).CreateEdit(); + spanTranslator.Received(1).TranslateTo(span1, snapshot, SpanTrackingMode.EdgeExclusive); + spanTranslator.Received(1).TranslateTo(span2, snapshot, SpanTrackingMode.EdgeExclusive); + textEdit.Received(1).Replace(translatedSpan1.Span, "new text 1"); + textEdit.Received(1).Replace(translatedSpan2.Span, "new text 2"); + issueViz.Received().Span = Arg.Is(x => x.Length == 0); + textEdit.Received(1).Apply(); + } + + [TestMethod] + public async Task ApplyAsync_CancellationRequested_DoesNotApplyChanges() + { + var cancellationToken = new CancellationToken(true); + var span = new SnapshotSpan(snapshot, new Span(1, 10)); + SetupEditVisualizations((span, "new text", span)); + + var act = () => testSubject.ApplyAsync(snapshot, issueViz, cancellationToken); + await act.Should().ThrowAsync(); + + textBuffer.Received(1).CreateEdit(); + textEdit.DidNotReceiveWithAnyArgs().Apply(); + issueViz.DidNotReceiveWithAnyArgs().Span = default; + } + + private void SetupTextBufferAndEdit() + { + textEdit = Substitute.For(); + textBuffer = Substitute.For(); + textBuffer.CreateEdit().Returns(textEdit); + textBuffer.CurrentSnapshot.Returns(snapshot); + snapshot.TextBuffer.Returns(textBuffer); + } + + private void SetupEditVisualizations(params (SnapshotSpan span, string newText, SnapshotSpan translatedSpan)[] edits) + { + var editVisualizations = new ITextBasedQuickFixEditVisualization[edits.Length]; + + for (int i = 0; i < edits.Length; i++) + { + var edit = Substitute.For(); + edit.Span.Returns(edits[i].span); + edit.Edit.NewText.Returns(edits[i].newText); + editVisualizations[i] = edit; + + spanTranslator + .TranslateTo(edits[i].span, snapshot, SpanTrackingMode.EdgeExclusive) + .Returns(edits[i].translatedSpan); + } + + quickFixVisualization.EditVisualizations.Returns(editVisualizations); + } +} diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs index ed79ea9b4f..72f36383bd 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs @@ -76,7 +76,7 @@ public IEnumerable GetSuggestedActions( { var applicableFixes = issueViz.QuickFixes.Where(x => x.CanBeApplied(textBuffer.CurrentSnapshot)); - allActions.AddRange(applicableFixes.Select(fix => new QuickFixSuggestedAction(fix, textBuffer, issueViz, quickFixesTelemetryManager, logger))); + allActions.AddRange(applicableFixes.Select(fix => new QuickFixSuggestedAction(fix, textBuffer, issueViz, quickFixesTelemetryManager, logger, threadHandling))); } } } diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs index 114f39fa3c..9edc10656f 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs @@ -21,47 +21,21 @@ using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; -using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Models; namespace SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes { - internal class QuickFixSuggestedAction : BaseSuggestedAction + internal class QuickFixSuggestedAction( + IQuickFixApplication quickFixApplication, + ITextBuffer textBuffer, + IAnalysisIssueVisualization issueViz, + IQuickFixesTelemetryManager quickFixesTelemetryManager, + ILogger logger, + IThreadHandling threadHandling) + : BaseSuggestedAction { - private readonly IQuickFixVisualization quickFixVisualization; - private readonly ITextBuffer textBuffer; - private readonly ISpanTranslator spanTranslator; - private readonly IAnalysisIssueVisualization issueViz; - private readonly IQuickFixesTelemetryManager quickFixesTelemetryManager; - private readonly ILogger logger; - public QuickFixSuggestedAction( - IQuickFixVisualization quickFixVisualization, - ITextBuffer textBuffer, - IAnalysisIssueVisualization issueViz, - IQuickFixesTelemetryManager quickFixesTelemetryManager, - ILogger logger) - : this(quickFixVisualization, textBuffer, issueViz, quickFixesTelemetryManager, logger, new SpanTranslator()) - { - } - - internal QuickFixSuggestedAction( - IQuickFixVisualization quickFixVisualization, - ITextBuffer textBuffer, - IAnalysisIssueVisualization issueViz, - IQuickFixesTelemetryManager quickFixesTelemetryManager, - ILogger logger, - ISpanTranslator spanTranslator) - { - this.quickFixVisualization = quickFixVisualization; - this.textBuffer = textBuffer; - this.issueViz = issueViz; - this.quickFixesTelemetryManager = quickFixesTelemetryManager; - this.logger = logger; - this.spanTranslator = spanTranslator; - } - - public override string DisplayText => Resources.ProductNameCommandPrefix + quickFixVisualization.Fix.Message; + public override string DisplayText => Resources.ProductNameCommandPrefix + quickFixApplication.Message; public override void Invoke(CancellationToken cancellationToken) { @@ -70,25 +44,21 @@ public override void Invoke(CancellationToken cancellationToken) return; } - if (!quickFixVisualization.CanBeApplied(textBuffer.CurrentSnapshot)) + if (!quickFixApplication.CanBeApplied(textBuffer.CurrentSnapshot)) { logger.LogVerbose("[Quick Fixes] Quick fix cannot be applied as the text has changed. Issue: " + issueViz.RuleId); return; } - var textEdit = textBuffer.CreateEdit(); - - foreach (var edit in quickFixVisualization.EditVisualizations) + threadHandling.Run(async () => { - var updatedSpan = spanTranslator.TranslateTo(edit.Span, textBuffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive); - - textEdit.Replace(updatedSpan, edit.Edit.NewText); - } + await threadHandling.SwitchToMainThreadAsync(); + await quickFixApplication.ApplyAsync(textBuffer.CurrentSnapshot, issueViz, cancellationToken); - issueViz.InvalidateSpan(); - textEdit.Apply(); + quickFixesTelemetryManager.QuickFixApplied(issueViz.RuleId); - quickFixesTelemetryManager.QuickFixApplied(issueViz.RuleId); + return 0; + }); } } } diff --git a/src/IssueViz/Models/AnalysisIssueVisualization.cs b/src/IssueViz/Models/AnalysisIssueVisualization.cs index 550a79c08a..b450f7bf3c 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualization.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualization.cs @@ -32,7 +32,7 @@ public interface IAnalysisIssueVisualization : IAnalysisIssueLocationVisualizati IAnalysisIssueBase Issue { get; } - IReadOnlyList QuickFixes { get; } + IReadOnlyList QuickFixes { get; } bool IsResolved { get; } } @@ -47,7 +47,7 @@ public AnalysisIssueVisualization( IReadOnlyList flows, IAnalysisIssueBase issue, SnapshotSpan? span, - IReadOnlyList quickFixes) + IReadOnlyList quickFixes) { Flows = flows; Issue = issue; @@ -57,7 +57,7 @@ public AnalysisIssueVisualization( } public IReadOnlyList Flows { get; } - public IReadOnlyList QuickFixes { get; } + public IReadOnlyList QuickFixes { get; } public IAnalysisIssueBase Issue { get; } public int StepNumber => 0; public IAnalysisIssueLocation Location => Issue.PrimaryLocation; diff --git a/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs b/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs index f3142dd009..c188330613 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs @@ -21,7 +21,9 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Core.CSharpVB; using SonarLint.VisualStudio.Core.Helpers; +using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.IssueVisualization.Editor; namespace SonarLint.VisualStudio.IssueVisualization.Models @@ -33,18 +35,11 @@ public interface IAnalysisIssueVisualizationConverter [Export(typeof(IAnalysisIssueVisualizationConverter))] [PartCreationPolicy(CreationPolicy.Shared)] - internal class AnalysisIssueVisualizationConverter : IAnalysisIssueVisualizationConverter + [method: ImportingConstructor] + internal class AnalysisIssueVisualizationConverter(IIssueSpanCalculator issueSpanCalculator, ISpanTranslator spanTranslator) : IAnalysisIssueVisualizationConverter { - private static readonly IReadOnlyList EmptyConvertedFlows = Array.Empty(); - private static readonly IReadOnlyList EmptyConvertedFixes = Array.Empty(); - - private readonly IIssueSpanCalculator issueSpanCalculator; - - [ImportingConstructor] - public AnalysisIssueVisualizationConverter(IIssueSpanCalculator issueSpanCalculator) - { - this.issueSpanCalculator = issueSpanCalculator; - } + private static readonly IReadOnlyList EmptyConvertedFlows = []; + private static readonly IReadOnlyList EmptyConvertedFixes = []; public IAnalysisIssueVisualization Convert(IAnalysisIssueBase issue, ITextSnapshot textSnapshot = null) { @@ -108,28 +103,40 @@ private IReadOnlyList Convert(IEnumerable GetQuickFixVisualizations(IAnalysisIssueBase issue, ITextSnapshot textSnapshot) + private IReadOnlyList GetQuickFixVisualizations(IAnalysisIssueBase issue, ITextSnapshot textSnapshot) { if (!(issue is IAnalysisIssue analysisIssue) || textSnapshot == null) { return EmptyConvertedFixes; } - var convertedQuickFixes = analysisIssue.Fixes.Select(fix => - { - var editVisualizations = fix.Edits.Select(edit => + return analysisIssue + .Fixes + .Select(fix => { - var editSpan = issueSpanCalculator.CalculateSpan(edit.RangeToReplace, textSnapshot) ?? new SnapshotSpan(); + if (fix is ITextBasedQuickFix textBasedQuickFix) + { + return HandleTextBasedQuickFix(textSnapshot, textBasedQuickFix); + } + return null; + }) + .Where(x => x is not null).ToArray(); + } - return new QuickFixEditVisualization(edit, editSpan); - }); + private TextBasedQuickFixApplication HandleTextBasedQuickFix( + ITextSnapshot textSnapshot, + ITextBasedQuickFix textBasedQuickFix) + { + var editVisualizations = textBasedQuickFix.Edits.Select(edit => + { + var editSpan = issueSpanCalculator.CalculateSpan(edit.RangeToReplace, textSnapshot) ?? new SnapshotSpan(); - var fixVisualization = new QuickFixVisualization(fix, editVisualizations.ToArray()); + return new TextBasedQuickFixEditVisualization(edit, editSpan); + }); - return fixVisualization; - }).ToArray(); + var fixVisualization = new TextBasedQuickFixVisualization(textBasedQuickFix, editVisualizations.ToArray(), spanTranslator); - return convertedQuickFixes; + return new TextBasedQuickFixApplication(fixVisualization, spanTranslator); } } } diff --git a/src/IssueViz/Models/IQuickFixApplication.cs b/src/IssueViz/Models/IQuickFixApplication.cs new file mode 100644 index 0000000000..6e18d8aed0 --- /dev/null +++ b/src/IssueViz/Models/IQuickFixApplication.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Text; + +namespace SonarLint.VisualStudio.IssueVisualization.Models; + +public interface IQuickFixApplication +{ + string Message { get; } + bool CanBeApplied(ITextSnapshot currentSnapshot); + Task ApplyAsync(ITextSnapshot currentSnapshot, IAnalysisIssueVisualization issueViz, CancellationToken cancellationToken); +} diff --git a/src/IssueViz/Models/QuickFixVisualization.cs b/src/IssueViz/Models/ITextBasedQuickFixVisualization.cs similarity index 60% rename from src/IssueViz/Models/QuickFixVisualization.cs rename to src/IssueViz/Models/ITextBasedQuickFixVisualization.cs index eba3536bc8..1975c42f0f 100644 --- a/src/IssueViz/Models/QuickFixVisualization.cs +++ b/src/IssueViz/Models/ITextBasedQuickFixVisualization.cs @@ -18,20 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Infrastructure.VS; namespace SonarLint.VisualStudio.IssueVisualization.Models { - public interface IQuickFixVisualization + public interface ITextBasedQuickFixVisualization { - IQuickFix Fix { get; } + ITextBasedQuickFix Fix { get; } - IReadOnlyList EditVisualizations { get; } + IReadOnlyList EditVisualizations { get; } /// /// Returns false if the snapshot has been edited so the fix can no longer be applied, otherwise true. @@ -39,34 +36,20 @@ public interface IQuickFixVisualization bool CanBeApplied(ITextSnapshot currentSnapshot); } - internal class QuickFixVisualization : IQuickFixVisualization + internal class TextBasedQuickFixVisualization( + ITextBasedQuickFix fix, + IReadOnlyList editVisualizations, + ISpanTranslator spanTranslator) + : ITextBasedQuickFixVisualization { - private readonly ISpanTranslator spanTranslator; + public ITextBasedQuickFix Fix { get; } = fix; - public QuickFixVisualization(IQuickFix fix, IReadOnlyList editVisualizations) - : this(fix, editVisualizations, new SpanTranslator()) - { - Fix = fix; - EditVisualizations = editVisualizations; - } - - internal QuickFixVisualization(IQuickFix fix, - IReadOnlyList editVisualizations, - ISpanTranslator spanTranslator) - { - this.spanTranslator = spanTranslator; - Fix = fix; - EditVisualizations = editVisualizations; - } - - public IQuickFix Fix { get; } - - public IReadOnlyList EditVisualizations { get; } + public IReadOnlyList EditVisualizations { get; } = editVisualizations; public bool CanBeApplied(ITextSnapshot currentSnapshot) => EditVisualizations.All(x => IsTextUnchanged(x, currentSnapshot)); - private bool IsTextUnchanged(IQuickFixEditVisualization editViz, ITextSnapshot currentSnapshot) + private bool IsTextUnchanged(ITextBasedQuickFixEditVisualization editViz, ITextSnapshot currentSnapshot) { var originalText = editViz.Span.GetText(); var updatedSpan = spanTranslator.TranslateTo(editViz.Span, currentSnapshot, SpanTrackingMode.EdgeExclusive); @@ -76,16 +59,16 @@ private bool IsTextUnchanged(IQuickFixEditVisualization editViz, ITextSnapshot c } } - public interface IQuickFixEditVisualization + public interface ITextBasedQuickFixEditVisualization { IEdit Edit { get; } SnapshotSpan Span { get; } } - internal class QuickFixEditVisualization : IQuickFixEditVisualization + internal class TextBasedQuickFixEditVisualization : ITextBasedQuickFixEditVisualization { - public QuickFixEditVisualization(IEdit edit, SnapshotSpan span) + public TextBasedQuickFixEditVisualization(IEdit edit, SnapshotSpan span) { Edit = edit; Span = span; diff --git a/src/IssueViz/Models/TextBasedQuickFixApplication.cs b/src/IssueViz/Models/TextBasedQuickFixApplication.cs new file mode 100644 index 0000000000..4fe9a0a203 --- /dev/null +++ b/src/IssueViz/Models/TextBasedQuickFixApplication.cs @@ -0,0 +1,52 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Text; +using SonarLint.VisualStudio.Infrastructure.VS; + +namespace SonarLint.VisualStudio.IssueVisualization.Models; + +internal class TextBasedQuickFixApplication(ITextBasedQuickFixVisualization textBasedQuickFixVisualization, ISpanTranslator spanTranslator) : IQuickFixApplication +{ + public ITextBasedQuickFixVisualization QuickFixVisualization { get; } = textBasedQuickFixVisualization; + + public string Message => QuickFixVisualization.Fix.Message; + + public bool CanBeApplied(ITextSnapshot currentSnapshot) => QuickFixVisualization.CanBeApplied(currentSnapshot); + + public Task ApplyAsync(ITextSnapshot currentSnapshot, IAnalysisIssueVisualization issueViz, CancellationToken cancellationToken) + { + var textBuffer = currentSnapshot.TextBuffer; + var textEdit = textBuffer.CreateEdit(); + + foreach (var edit in QuickFixVisualization.EditVisualizations) + { + var updatedSpan = spanTranslator.TranslateTo(edit.Span, textBuffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive); + + textEdit.Replace(updatedSpan, edit.Edit.NewText); + } + + cancellationToken.ThrowIfCancellationRequested(); + + issueViz.InvalidateSpan(); + textEdit.Apply(); + return Task.CompletedTask; + } +} diff --git a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs index 52bcc985f6..d96aa5cab1 100644 --- a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs +++ b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs @@ -382,7 +382,7 @@ public void GetAnalysisIssues_IssueWithQuickFixSplitIntoTwoFileEdits_ReturnsIssu analysisIssues.Should().NotBeNull(); analysisIssues.Should().ContainSingle(); analysisIssues[0].Fixes.Should().ContainSingle(); - analysisIssues[0].Fixes[0].Edits.Should().HaveCount(2); + analysisIssues[0].Fixes[0].Should().BeAssignableTo().Which.Edits.Should().HaveCount(2); } [TestMethod] @@ -622,13 +622,14 @@ private static void VerifyIssue2ConvertedCorrectly(List result) result[1].Flows[1].Locations[1].TextRange.LineHash.Should().BeNull(); result[1].Fixes.Should().HaveCount(1); - result[1].Fixes[0].Message.Should().Be("issue 2 fix 2"); - result[1].Fixes[0].Edits.Should().HaveCount(1); - result[1].Fixes[0].Edits[0].RangeToReplace.StartLine.Should().Be(51); - result[1].Fixes[0].Edits[0].RangeToReplace.StartLineOffset.Should().Be(52); - result[1].Fixes[0].Edits[0].RangeToReplace.EndLine.Should().Be(53); - result[1].Fixes[0].Edits[0].RangeToReplace.EndLineOffset.Should().Be(54); - result[1].Fixes[0].Edits[0].RangeToReplace.LineHash.Should().BeNull(); + var quickFix = result[1].Fixes[0].Should().BeAssignableTo().Subject; + quickFix.Message.Should().Be("issue 2 fix 2"); + quickFix.Edits.Should().HaveCount(1); + quickFix.Edits[0].RangeToReplace.StartLine.Should().Be(51); + quickFix.Edits[0].RangeToReplace.StartLineOffset.Should().Be(52); + quickFix.Edits[0].RangeToReplace.EndLine.Should().Be(53); + quickFix.Edits[0].RangeToReplace.EndLineOffset.Should().Be(54); + quickFix.Edits[0].RangeToReplace.LineHash.Should().BeNull(); } } } diff --git a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs index 25eb119d67..40eec6d75e 100644 --- a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs +++ b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs @@ -38,9 +38,9 @@ public IEnumerable GetAnalysisIssues(FileUri fileUri, IEnumer raisedFindings .Select(item => TryCreateAnalysisIssue(fileUri, item)) .Where(x => x != null) - .ToList(); + .ToList()!; - private AnalysisIssue TryCreateAnalysisIssue(FileUri fileUri, T item) where T : RaisedFindingDto + private AnalysisIssue? TryCreateAnalysisIssue(FileUri fileUri, T item) where T : RaisedFindingDto { try { @@ -89,7 +89,7 @@ private AnalysisIssue TryCreateAnalysisIssue(FileUri fileUri, T item) where T } } - private static Impact GetHighestImpact(List impacts) + private static Impact? GetHighestImpact(List? impacts) { if (impacts is null || impacts.Count == 0) { @@ -101,7 +101,7 @@ private static Impact GetHighestImpact(List impacts) private static IAnalysisIssueLocation GetAnalysisIssueLocation(string filePath, string message, TextRangeDto textRangeDto) => new AnalysisIssueLocation(message, filePath, CopyTextRange(textRangeDto)); - private static TextRange CopyTextRange(TextRangeDto textRangeDto) + private static TextRange? CopyTextRange(TextRangeDto? textRangeDto) { if (textRangeDto is null) { @@ -114,7 +114,7 @@ private static TextRange CopyTextRange(TextRangeDto textRangeDto) null); } - private static IAnalysisIssueFlow[] GetFlows(List issueFlows) + private static IAnalysisIssueFlow[] GetFlows(List? issueFlows) { if (issueFlows is null || issueFlows.Count == 0) { @@ -132,7 +132,7 @@ private static IAnalysisIssueFlow[] GetFlows(List issueFlows) private static IAnalysisIssueFlow GetAnalysisIssueFlow(IEnumerable flowLocations) => new AnalysisIssueFlow(flowLocations.Select(l => GetAnalysisIssueLocation(l.fileUri.LocalPath, l.message, l.textRange)).ToList()); - private static IQuickFix? GetQuickFix(FileUri fileURi, QuickFixDto quickFixDto) + private static IQuickFixBase? GetQuickFix(FileUri fileURi, QuickFixDto quickFixDto) { var fileEdits = quickFixDto.inputFileEdits.FindAll(e => e.target == fileURi); if (fileEdits.Count == 0) @@ -141,7 +141,7 @@ private static IAnalysisIssueFlow GetAnalysisIssueFlow(IEnumerable x.textEdits).Select(GetEdit).ToList(); - return new QuickFix(quickFixDto.message, textEdits); + return new TextBasedQuickFix(quickFixDto.message, textEdits); } private static IEdit GetEdit(TextEditDto textEdit) => diff --git a/src/TestInfrastructure/DummyAnalysisIssue.cs b/src/TestInfrastructure/DummyAnalysisIssue.cs index 5f4d1199d3..41db7abcac 100644 --- a/src/TestInfrastructure/DummyAnalysisIssue.cs +++ b/src/TestInfrastructure/DummyAnalysisIssue.cs @@ -41,5 +41,5 @@ public class DummyAnalysisIssue : IAnalysisIssue public bool IsResolved { get; set; } public string IssueServerKey { get; set; } - public IReadOnlyList Fixes { get; } = Array.Empty(); + public IReadOnlyList Fixes { get; } = []; } From c626d96ba36917a354006109c9d2fc0be5fee588 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 3 Sep 2025 11:53:42 +0200 Subject: [PATCH 20/38] SLVS-2502 Disable old roslyn analysis, configuration files updates and talking to the server (#6397) --- .../RoslynBindingConfigProviderTests.cs | 269 ----------- .../BoundSolutionUpdateHandlerTests.cs | 88 ---- .../Migration/ConnectedModeMigrationTests.cs | 16 - .../OutOfDateQualityProfileFinderTests.cs | 233 --------- .../RoslynQualityProfileDownloaderTests.cs | 378 --------------- .../ServerIssueFinderTests.cs | 178 ------- .../Issue/IssueServerEventsListenerTests.cs | 199 -------- .../RoslynSuppressionUpdaterTests.cs | 391 --------------- .../Suppressions/TimedUpdateHandlerTests.cs | 179 ------- .../Transition/MuteIssuesServiceTests.cs | 127 +---- .../Binding/IBindingConfigProvider.cs | 37 -- .../Binding/RoslynBindingConfigProvider.cs | 143 ------ .../BoundSolutionUpdateHandler.cs | 73 --- src/ConnectedMode/ConnectedModePackage.cs | 16 - .../Migration/ConnectedModeMigration.cs | 7 - .../OutOfDateQualityProfileFinder.cs | 85 ---- .../RoslynQualityProfileDownloader.cs | 87 +--- src/ConnectedMode/ServerIssueFinder.cs | 95 ---- .../Issue/IssueServerEventsListener.cs | 116 ----- .../Suppressions/IRoslynSuppressionUpdater.cs | 54 --- .../Suppressions/RoslynSuppressionUpdater.cs | 144 ------ .../Suppressions/TimedUpdateHandler.cs | 112 ----- .../Transition/MuteIssuesService.cs | 40 +- src/Core/CSharpVB/IRoslynConfigGenerator.cs | 32 -- .../SolutionRoslynAnalyzerManagerTests.cs | 381 --------------- .../Roslyn/SolutionRoslynAnalyzerManager.cs | 193 -------- .../StandaloneRoslynSettingsUpdaterTests.cs | 222 --------- .../Analysis/IssueConsumerFactory.cs | 7 +- .../SonarLintDaemonPackage.cs | 5 - .../SonarLintIntegrationPackage.cs | 13 - .../CSharpVB/RoslynConfigGenerator.cs | 68 --- .../StandaloneRoslynSettingsUpdater.cs | 75 +-- .../RoslynSettingsFileSynchronizerTests.cs | 405 ---------------- .../RoslynSettingsFileSynchronizer.cs | 188 -------- .../SonarQubeService_GetAllPropertiesAsync.cs | 219 --------- ...rQubeService_GetAllQualityProfilesAsync.cs | 324 ------------- .../SonarQubeService_GetExclusionsRequest.cs | 82 ---- .../SonarQubeService_GetIssuesForComponent.cs | 86 ---- .../SonarQubeService_GetRulesAsync.cs | 453 ------------------ ...eService_GetSuppressedRoslynIssuesAsync.cs | 297 ------------ src/SonarQube.Client/ISonarQubeService.cs | 59 --- src/SonarQube.Client/SonarQubeService.cs | 65 --- 42 files changed, 25 insertions(+), 6216 deletions(-) delete mode 100644 src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs delete mode 100644 src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs delete mode 100644 src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs delete mode 100644 src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs delete mode 100644 src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs delete mode 100644 src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs delete mode 100644 src/ConnectedMode/Binding/IBindingConfigProvider.cs delete mode 100644 src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs delete mode 100644 src/ConnectedMode/BoundSolutionUpdateHandler.cs delete mode 100644 src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs delete mode 100644 src/ConnectedMode/ServerIssueFinder.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs delete mode 100644 src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs delete mode 100644 src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs delete mode 100644 src/ConnectedMode/Suppressions/TimedUpdateHandler.cs delete mode 100644 src/Core/CSharpVB/IRoslynConfigGenerator.cs delete mode 100644 src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs delete mode 100644 src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs delete mode 100644 src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs delete mode 100644 src/Integration/CSharpVB/RoslynConfigGenerator.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs diff --git a/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs b/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs deleted file mode 100644 index e2bcd45ff8..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/RoslynBindingConfigProviderTests.cs +++ /dev/null @@ -1,269 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; -using Language = SonarLint.VisualStudio.Core.Language; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; - -[TestClass] -public class RoslynBindingConfigProviderTests -{ - private IList validRules; - private IList anyProperties; - private SonarQubeQualityProfile validQualityProfile; - - private static readonly SonarQubeRule ActiveRuleWithUnsupportedSeverity = new SonarQubeRule("activeHotspot", "any1", - true, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.SecurityHotspot); - - private static readonly SonarQubeRule InactiveRuleWithUnsupportedSeverity = new SonarQubeRule("inactiveHotspot", "any2", - false, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.SecurityHotspot); - - private static readonly SonarQubeRule ActiveTaintAnalysisRule = new SonarQubeRule("activeTaint", "roslyn.sonaranalyzer.security.foo", - true, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.CodeSmell); - - private static readonly SonarQubeRule InactiveTaintAnalysisRule = new SonarQubeRule("inactiveTaint", "roslyn.sonaranalyzer.security.bar", - false, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.CodeSmell); - - [TestInitialize] - public void TestInitialize() - { - validRules = new List { new SonarQubeRule("key", "repoKey", true, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.Bug) }; - - anyProperties = Array.Empty(); - - validQualityProfile = new SonarQubeQualityProfile("qpkey1", "qp name", "any", false, DateTime.UtcNow); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => - MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void GetRules_UnsupportedLanguage_Throws() - { - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.Cpp); - var testSubject = builder.CreateTestSubject(); - - // Act - Action act = () => testSubject.SaveConfigurationAsync(validQualityProfile, FakeRoslynLanguage.Instance, BindingConfiguration.Standalone, CancellationToken.None).Wait(); - - // Assert - act.Should().ThrowExactly().And.ParamName.Should().Be("language"); - } - - [TestMethod] - public void IsLanguageSupported() - { - // Arrange - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.Cpp); - var testSubject = builder.CreateTestSubject(); - - // 1. Supported languages - testSubject.IsLanguageSupported(Language.CSharp).Should().BeTrue(); - testSubject.IsLanguageSupported(Language.VBNET).Should().BeTrue(); - - // 2. Not supported - testSubject.IsLanguageSupported(Language.C).Should().BeFalse(); - testSubject.IsLanguageSupported(Language.Cpp).Should().BeFalse(); - - testSubject.IsLanguageSupported(Language.Unknown).Should().BeFalse(); - } - - [TestMethod] - public async Task GetConfig_NoSupportedActiveRules_Throws() - { - // Arrange - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.VBNET) - { - ActiveRulesResponse = new List { ActiveRuleWithUnsupportedSeverity }, InactiveRulesResponse = validRules, PropertiesResponse = anyProperties - }; - var testSubject = builder.CreateTestSubject(); - - // Act - var act = () => testSubject.SaveConfigurationAsync(validQualityProfile, Language.VBNET, builder.BindingConfiguration, CancellationToken.None); - - // Assert - await act.Should().ThrowExactlyAsync().WithMessage(string.Format(QualityProfilesStrings.FailedToCreateBindingConfigForLanguage, Language.VBNET.Name)); - - builder.Logger.AssertOutputStrings(1); - var expectedOutput = string.Format(BindingStrings.SubTextPaddingFormat, - string.Format(BindingStrings.NoSonarAnalyzerActiveRulesForQualityProfile, validQualityProfile.Name, Language.VBNET.Name)); - builder.Logger.AssertOutputStrings(expectedOutput); - builder.RoslynConfigGenerator.DidNotReceiveWithAnyArgs().GenerateAndSaveConfiguration(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any>(), Arg.Any>()); - } - - [TestMethod] - public async Task GetConfig_HasActiveInactiveAndUnsupportedRules_ReturnsValidBindingConfig() - { - // Arrange - const string expectedServerUrl = "http://myhost:123/"; - - var properties = new SonarQubeProperty[] { new("propertyAAA", "111"), new("propertyBBB", "222") }; - - var activeSupportedRule = CreateRule("activeRuleKey", "repoKey1", true); - var activeRules = new[] { activeSupportedRule, ActiveTaintAnalysisRule, ActiveRuleWithUnsupportedSeverity }; - var inactiveSupportedRule = CreateRule("inactiveRuleKey", "repoKey2", false); - var inactiveRules = new[] { inactiveSupportedRule, InactiveTaintAnalysisRule, InactiveRuleWithUnsupportedSeverity }; - - var builder = new TestEnvironmentBuilder(validQualityProfile, Language.CSharp, expectedServerUrl) - { - ActiveRulesResponse = activeRules, InactiveRulesResponse = inactiveRules, PropertiesResponse = properties - }; - - var testSubject = builder.CreateTestSubject(); - - // Act - await testSubject.SaveConfigurationAsync(validQualityProfile, Language.CSharp, builder.BindingConfiguration, CancellationToken.None); - - // Assert - builder.RoslynConfigGenerator - .Received() - .GenerateAndSaveConfiguration( - Language.CSharp, - builder.BindingConfiguration.BindingConfigDirectory, - Arg.Is>(x => x.SequenceEqual(builder.SonarProperties)), - builder.ServerExclusionsResponse, - Arg.Is((IReadOnlyCollection x) => - x.Select(y => y.Key).SequenceEqual(new []{activeSupportedRule.Key, inactiveSupportedRule.Key})), - Arg.Is>(x => x.SequenceEqual(new []{activeSupportedRule}))); - builder.Logger.AssertOutputStrings(0); // not expecting anything in the case of success - } - - [TestMethod] - [DataRow("roslyn.sonaranalyzer.security.cs", false)] - [DataRow("roslyn.sonaranalyzer.security.vb", false)] - [DataRow("ROSLYN.SONARANALYZER.SECURITY.X", false)] - [DataRow("roslyn.wintellect", true)] - [DataRow("sonaranalyzer-cs", true)] - [DataRow("sonaranalyzer-vbnet", true)] - public void IsSupportedRule_TaintRules(string repositoryKey, bool expected) - { - var rule = CreateRule("any", repositoryKey, true); - - RoslynBindingConfigProvider.IsSupportedRule(rule).Should().Be(expected); - } - - [TestMethod] - [DataRow(SonarQubeIssueType.Unknown, false)] - [DataRow(SonarQubeIssueType.SecurityHotspot, false)] - [DataRow(SonarQubeIssueType.CodeSmell, true)] - [DataRow(SonarQubeIssueType.Bug, true)] - [DataRow(SonarQubeIssueType.Vulnerability, true)] - public void IsSupportedRule_Severity(SonarQubeIssueType issueType, bool expected) - { - var rule = new SonarQubeRule("any", "any", true, SonarQubeIssueSeverity.Blocker, null, null, null, issueType); - - RoslynBindingConfigProvider.IsSupportedRule(rule).Should().Be(expected); - } - - private static SonarQubeRule CreateRule(string ruleKey, string repoKey, bool isActive) => - new SonarQubeRule(ruleKey, repoKey, isActive, SonarQubeIssueSeverity.Blocker, null, null, null, SonarQubeIssueType.CodeSmell); - - private class TestEnvironmentBuilder - { - private Mock sonarQubeServiceMock; - - private readonly SonarQubeQualityProfile profile; - private readonly Language language; - private readonly string serverUrl; - - private const string ExpectedProjectKey = "fixed.project.key"; - - public TestEnvironmentBuilder(SonarQubeQualityProfile profile, Language language, string serverUrl = "http://any") - { - this.profile = profile; - this.language = language; - this.serverUrl = serverUrl; - - Logger = new TestLogger(); - PropertiesResponse = new List(); - } - - public BindingConfiguration BindingConfiguration { get; private set; } - public IRoslynConfigGenerator RoslynConfigGenerator { get; private set; } - public IList ActiveRulesResponse { get; set; } - public IList InactiveRulesResponse { get; set; } - public IList PropertiesResponse { get; set; } - - public ServerExclusions ServerExclusionsResponse { get; set; } - public Dictionary SonarProperties { get; set; } - public TestLogger Logger { get; private set; } - - public RoslynBindingConfigProvider CreateTestSubject() - { - // Note: where possible, the mocked methods are set up with the expected - // parameter values i.e. they will only be called if the correct values - // are passed in. - Logger = new TestLogger(); - - sonarQubeServiceMock = new Mock(); - sonarQubeServiceMock - .Setup(x => x.GetRulesAsync(true, profile.Key, It.IsAny())) - .ReturnsAsync(ActiveRulesResponse); - - sonarQubeServiceMock - .Setup(x => x.GetRulesAsync(false, profile.Key, It.IsAny())) - .ReturnsAsync(InactiveRulesResponse); - - sonarQubeServiceMock - .Setup(x => x.GetAllPropertiesAsync(ExpectedProjectKey, It.IsAny())) - .ReturnsAsync(PropertiesResponse); - - ServerExclusionsResponse = new ServerExclusions( - exclusions: ["path1"], - globalExclusions: ["path2"], - inclusions: ["path3"]); - - sonarQubeServiceMock - .Setup(x => x.GetServerExclusions(ExpectedProjectKey, It.IsAny())) - .ReturnsAsync(ServerExclusionsResponse); - - BindingConfiguration = new BindingConfiguration( - new BoundServerProject("solution", ExpectedProjectKey, new ServerConnection.SonarQube(new Uri(serverUrl))), - SonarLintMode.Connected, - "c:\\test\\"); - - SonarProperties = PropertiesResponse.ToDictionary(x => x.Key, y => y.Value); - - RoslynConfigGenerator = Substitute.For(); - - return new RoslynBindingConfigProvider(sonarQubeServiceMock.Object, Logger, - RoslynConfigGenerator, - LanguageProvider.Instance); - } - } -} diff --git a/src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs b/src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs deleted file mode 100644 index 7f9f091c53..0000000000 --- a/src/ConnectedMode.UnitTests/BoundSolutionUpdateHandlerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests -{ - [TestClass] - public class BoundSolutionUpdateHandlerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_SubscribesToEvents() - { - var activeSolutionTracker = new Mock(); - - _ = new BoundSolutionUpdateHandler(activeSolutionTracker.Object, Mock.Of(), Mock.Of()); - - activeSolutionTracker.VerifyAdd(x => x.SolutionBindingChanged += It.IsAny>(), Times.Once); - activeSolutionTracker.VerifyAdd(x => x.SolutionBindingUpdated += It.IsAny(), Times.Once); - } - - [TestMethod] - public void InvokeEvents_ServerStoreUpdatersAreCalled() - { - var activeSolutionTracker = new Mock(); - var suppressionUpdater = new Mock(); - var qualityProfileUpdater = new Mock(); - - _ = new BoundSolutionUpdateHandler(activeSolutionTracker.Object, suppressionUpdater.Object, qualityProfileUpdater.Object); - - activeSolutionTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(BindingConfiguration.Standalone)); - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Once); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Once); - - activeSolutionTracker.Raise(x => x.SolutionBindingUpdated += null, EventArgs.Empty); - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Exactly(2)); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Exactly(2)); - } - - [TestMethod] - public void Dispose_UnsubscribesToEvent() - { - var activeSolutionTracker = new Mock(); - - var testSubject = new BoundSolutionUpdateHandler(activeSolutionTracker.Object, Mock.Of(), Mock.Of()); - - testSubject.Dispose(); - - activeSolutionTracker.VerifyRemove(x => x.SolutionBindingChanged -= It.IsAny>(), Times.Once); - activeSolutionTracker.VerifyRemove(x => x.SolutionBindingUpdated -= It.IsAny(), Times.Once); - } - } -} diff --git a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs index 8b8b7ee603..428eb7e774 100644 --- a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs @@ -21,7 +21,6 @@ using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Migration; using SonarLint.VisualStudio.ConnectedMode.Shared; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.ConnectedMode.UnitTests.Migration.ConnectedModeMigrationTestsExtensions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; @@ -51,7 +50,6 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), @@ -449,17 +447,6 @@ public void Migrate_InvalidServerInformation_Throws() act.Should().Throw(); } - [TestMethod] - public async Task Migrate_RoslynSuppressionUpdateIsTriggered() - { - var suppressionsUpdater = new Mock(); - - var testSubject = CreateTestSubject(roslynSuppressionUpdater: suppressionsUpdater.Object); - await testSubject.MigrateAsync(AnyBoundProject, null, false, CancellationToken.None); - - suppressionsUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Once); - } - [TestMethod] public async Task Migrate_SwitchToBackgroundThread() { @@ -480,7 +467,6 @@ private static ConnectedModeMigration CreateTestSubject( IMigrationSettingsProvider settingsProvider = null, ISonarQubeService sonarQubeService = null, IUnintrusiveBindingController unintrusiveBindingController = null, - IRoslynSuppressionUpdater roslynSuppressionUpdater = null, ISharedBindingConfigProvider sharedBindingConfigProvider = null, ILogger logger = null, IThreadHandling threadHandling = null, @@ -493,7 +479,6 @@ private static ConnectedModeMigration CreateTestSubject( fileSystem ??= Mock.Of(); sonarQubeService ??= Mock.Of(); unintrusiveBindingController ??= Mock.Of(); - roslynSuppressionUpdater ??= Mock.Of(); settingsProvider ??= CreateSettingsProvider(DefaultTestLegacySettings).Object; sharedBindingConfigProvider ??= Mock.Of(); solutionInfoProvider ??= CreateSolutionInfoProviderMock().Object; @@ -509,7 +494,6 @@ private static ConnectedModeMigration CreateTestSubject( fileSystem, sonarQubeService, unintrusiveBindingController, - roslynSuppressionUpdater, sharedBindingConfigProvider, logger, threadHandling, diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs deleted file mode 100644 index 32e0eef308..0000000000 --- a/src/ConnectedMode.UnitTests/QualityProfiles/OutOfDateQualityProfileFinderTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.QualityProfiles; - -[TestClass] -public class OutOfDateQualityProfileFinderTests -{ - private static readonly Uri AnyUri = new("http://localhost"); - private const string Project = "project"; - private const string Organization = "organization"; - - [TestMethod] - public void MefCtor_CheckExports() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Mef_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task GetAsync_SkipsUnknownLanguage() - { - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - new SonarQubeQualityProfile("key", "name", "unsupportedlanguage", false, DateTime.UtcNow)); - - var profiles = await testSubject.GetAsync(CreateArgument(Project, Organization, null), CancellationToken.None); - - profiles.Should().BeEmpty(); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_SkipsUpToDateQualityProfile() - { - const string qpKey = "key"; - var timestamp = DateTime.UtcNow; - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, timestamp)); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = qpKey, ProfileTimestamp = timestamp } } }), - CancellationToken.None); - - profiles.Should().BeEmpty(); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_WhenBoundProjectDoesNotContainLanguage_SkipsUpToDateQualityProfile() - { - const string qpKey = "key"; - var timestamp = DateTime.UtcNow; - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, timestamp)); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary()), - CancellationToken.None); - - profiles.Should().BeEmpty(); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_DifferentKey_ReturnsQP() - { - const string serverQpKey = "key1"; - const string localQpKey = "key2"; - var timestampServer = DateTime.UtcNow; - var timestampLocal = timestampServer.AddHours(1); // local timestamp is greater, but the key is more important - var sonarQubeQualityProfile = new SonarQubeQualityProfile(serverQpKey, "name", Language.Ts.ServerLanguageKey, false, timestampServer); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - sonarQubeQualityProfile); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = localQpKey, ProfileTimestamp = timestampLocal } } }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.Ts, sonarQubeQualityProfile)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_OutdatedLocalTimestamp_ReturnsQP() - { - const string qpKey = "key"; - var localTimestamp = DateTime.UtcNow; - var serverTimestamp = localTimestamp.AddHours(1); - var sonarQubeQualityProfile = new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, serverTimestamp); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - sonarQubeQualityProfile); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = qpKey, ProfileTimestamp = localTimestamp } } }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.Ts, sonarQubeQualityProfile)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_NullLocalQP_ReturnsQP() - { - const string qpKey = "key"; - var serverTimestamp = DateTime.UtcNow; - var sonarQubeQualityProfile = new SonarQubeQualityProfile(qpKey, "name", Language.Ts.ServerLanguageKey, false, serverTimestamp); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - sonarQubeQualityProfile); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary { { Language.Ts, new ApplicableQualityProfile { ProfileKey = null, ProfileTimestamp = DateTime.MinValue } } }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.Ts, sonarQubeQualityProfile)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - [TestMethod] - public async Task GetAsync_NullOrganization_DoesNotThrow() - { - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, null); - - var act = () => testSubject.GetAsync( - CreateArgument(Project, - null, - new Dictionary()), - CancellationToken.None); - - await act.Should().NotThrowAsync(); - sonarQubeServiceMock.Verify( - x => - x.GetAllQualityProfilesAsync(Project, null, CancellationToken.None), - Times.Once); - } - - [TestMethod] - public async Task GetAsync_MultipleQualityProfiles_ReturnsQP() - { - var serverKey = "key"; - var localKey = "localKey"; - var timestamp = DateTime.UtcNow; - var csharpQp = new SonarQubeQualityProfile(serverKey, "name", Language.CSharp.ServerLanguageKey, false, timestamp); - var cssQp = new SonarQubeQualityProfile(serverKey, "name", Language.Css.ServerLanguageKey, false, timestamp); - var jsQp = new SonarQubeQualityProfile(serverKey, "name", Language.Js.ServerLanguageKey, false, timestamp); - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, Project, Organization, - csharpQp, - cssQp, - jsQp); - - var profiles = await testSubject.GetAsync( - CreateArgument(Project, - Organization, - new Dictionary - { - { Language.Js, new ApplicableQualityProfile { ProfileKey = localKey, ProfileTimestamp = timestamp } }, - { Language.CSharp, new ApplicableQualityProfile { ProfileKey = localKey, ProfileTimestamp = timestamp } }, - { Language.Css, new ApplicableQualityProfile { ProfileKey = serverKey, ProfileTimestamp = timestamp } } // same qp - }), - CancellationToken.None); - - profiles.Should().BeEquivalentTo((Language.CSharp, csharpQp), (Language.Js, jsQp)); - sonarQubeServiceMock.Verify(x => x.GetAllQualityProfilesAsync(Project, Organization, CancellationToken.None), Times.Once); - } - - private static BoundServerProject CreateArgument( - string project, - string organization, - Dictionary profiles) => - new("solution", - project, - organization == null ? new ServerConnection.SonarQube(AnyUri) : new ServerConnection.SonarCloud(organization)) { Profiles = profiles }; - - private IOutOfDateQualityProfileFinder CreateTestSubject( - out Mock sonarQubeServiceMock, - string project, - string organization, - params SonarQubeQualityProfile[] qualityProfiles) - { - sonarQubeServiceMock = new Mock(); - sonarQubeServiceMock - .Setup(x => x.GetAllQualityProfilesAsync(project, organization, It.IsAny())) - .Returns(Task.FromResult>(qualityProfiles)); - - return new OutOfDateQualityProfileFinder(sonarQubeServiceMock.Object, LanguageProvider.Instance); - } -} diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs deleted file mode 100644 index f22b9107ba..0000000000 --- a/src/ConnectedMode.UnitTests/QualityProfiles/RoslynQualityProfileDownloaderTests.cs +++ /dev/null @@ -1,378 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.QualityProfiles; - -[TestClass] -public class RoslynQualityProfileDownloaderTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public async Task UpdateAsync_NothingToUpdate_ReturnsFalse() - { - var boundSonarQubeProject = CreateBoundProject(); - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundSonarQubeProject, - Array.Empty()); - var logger = new TestLogger(); - - var bindingConfigProvider = new Mock(); - - var testSubject = CreateTestSubject(outOfDateQualityProfileFinderMock.Object, - bindingConfigProvider.Object, - logger: logger); - - var result = await testSubject.UpdateAsync(boundSonarQubeProject, null, CancellationToken.None); - - result.Should().BeFalse(); - bindingConfigProvider.Invocations.Should().BeEmpty(); - - logger.AssertPartialOutputStringExists(string.Format(QualityProfilesStrings.SubTextPaddingFormat, QualityProfilesStrings.DownloadingQualityProfilesNotNeeded)); - } - - [TestMethod] - public async Task UpdateAsync_MultipleQPs_ProgressEventsAreRaised() - { - // Arrange - var logger = new TestLogger(logToConsole: true); - var boundProject = CreateBoundProject(); - - var languagesToBind = new[] { Language.CSharp, Language.VBNET }; - - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - languagesToBind); - - var bindingConfigProviderMock = new Mock(); - SetupConfigSave(bindingConfigProviderMock, Language.CSharp); - SetupConfigSave(bindingConfigProviderMock, Language.VBNET); - - var testSubject = CreateTestSubject( - languageProvider: CreateLanguageProvider(languagesToBind).Object, - outOfDateQualityProfileFinder: outOfDateQualityProfileFinderMock.Object, - bindingConfigProvider: bindingConfigProviderMock.Object, - logger: logger); - - var notifications = new List(); - var progressAdapter = new Mock>(); - progressAdapter.Setup(x => x.Report(It.IsAny())) - .Callback(x => - (notifications).Add(x)); - - // Act - var result = await testSubject.UpdateAsync(boundProject, progressAdapter.Object, CancellationToken.None); - - // Assert - result.Should().BeTrue(); - - // Progress notifications - percentage complete and messages - notifications.Should().BeEquivalentTo(new[] - { - new FixedStepsProgress(string.Format(QualityProfilesStrings.DownloadingQualityProfileProgressMessage, Language.CSharp.Name), 1, 2), - new FixedStepsProgress(string.Format(QualityProfilesStrings.DownloadingQualityProfileProgressMessage, Language.VBNET.Name), 2, 2), - }); - } - - [TestMethod] - public async Task UpdateAsync_OnNewBoundProject_InitializesOnlyRoslynLanguages() - { - // Arrange - var boundProject = CreateBoundProject(); - var logger = new TestLogger(); - var progressAdapter = new Mock>(); - var mockLanguageProvider = CreateLanguageProvider(); - - var outOfDateQualityProfileFinder = new Mock(); - outOfDateQualityProfileFinder - .Setup(x => x.GetAsync(boundProject, It.IsAny())) - .ReturnsAsync([]); - - boundProject.Profiles.Should().BeNull(); - - var testSubject = CreateTestSubject( - bindingConfigProvider: new Mock().Object, - configurationPersister: new DummyConfigPersister(), - outOfDateQualityProfileFinder: outOfDateQualityProfileFinder.Object, - languageProvider: mockLanguageProvider.Object, - logger: logger); - - // Act - await testSubject.UpdateAsync(boundProject, progressAdapter.Object, CancellationToken.None); - - // Assert - boundProject.Profiles.Count.Should().Be(2); - boundProject.Profiles.ContainsKey(Language.VBNET).Should().BeTrue(); - boundProject.Profiles.ContainsKey(Language.CSharp).Should().BeTrue(); - mockLanguageProvider.Verify(x => x.RoslynLanguages, Times.Once); - mockLanguageProvider.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task UpdateAsync_UpdatesOnlyRoslynLanguages() - { - // Arrange - var boundProject = CreateBoundProject(); - var logger = new TestLogger(logToConsole: true); - - var mockLanguageProvider = CreateLanguageProvider(); - - // Configure available languages on the server - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - Language.CSharp, Language.VBNET, FakeRoslynLanguage.Instance); - - var configProvider = new Mock(MockBehavior.Strict); - SetupConfigSave(configProvider, Language.CSharp); - SetupConfigSave(configProvider, Language.VBNET); - SetupConfigSave(configProvider, FakeRoslynLanguage.Instance); - - var configPersister = new DummyConfigPersister(); - - var testSubject = CreateTestSubject( - bindingConfigProvider: configProvider.Object, - configurationPersister: configPersister, - outOfDateQualityProfileFinder: outOfDateQualityProfileFinderMock.Object, - languageProvider: mockLanguageProvider.Object, - logger: logger); - - // Act - var result = await testSubject.UpdateAsync(boundProject, Mock.Of>(), CancellationToken.None); - - // Assert - result.Should().BeTrue(); - - CheckRuleConfigSaved(configProvider, Language.CSharp); - CheckRuleConfigSaved(configProvider, Language.VBNET); - CheckRuleConfigNotSaved(configProvider, FakeRoslynLanguage.Instance); - - boundProject.Profiles.Count.Should().Be(2); - boundProject.Profiles[Language.VBNET].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[Language.CSharp].ProfileKey.Should().NotBeNull(); - mockLanguageProvider.Verify(x => x.RoslynLanguages, Times.Once); - mockLanguageProvider.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task UpdateAsync_WhenQualityProfileIsNotAvailable_OtherLanguagesDownloadedSuccessfully() - { - var boundProject = CreateBoundProject(); - // Arrange - var logger = new TestLogger(logToConsole: true); - var languagesToBind = new[] - { - FakeRoslynLanguage.Instance, // unavailable - Language.CSharp, - Language.VBNET - }; - - // Configure available languages on the server - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - Language.CSharp, - Language.VBNET); - - var configProvider = new Mock(MockBehavior.Strict); - SetupConfigSave(configProvider, FakeRoslynLanguage.Instance); - SetupConfigSave(configProvider, Language.CSharp); - SetupConfigSave(configProvider, Language.VBNET); - - var configPersister = new DummyConfigPersister(); - - var testSubject = CreateTestSubject( - bindingConfigProvider: configProvider.Object, - configurationPersister: configPersister, - languageProvider: CreateLanguageProvider(languagesToBind).Object, - outOfDateQualityProfileFinder: outOfDateQualityProfileFinderMock.Object, - logger: logger); - - // Act - var result = await testSubject.UpdateAsync(boundProject, Mock.Of>(), CancellationToken.None); - - // Assert - result.Should().BeTrue(); - - CheckRuleConfigSaved(configProvider, Language.CSharp); - CheckRuleConfigSaved(configProvider, Language.VBNET); - CheckRuleConfigNotSaved(configProvider, FakeRoslynLanguage.Instance); - - boundProject.Profiles.Count.Should().Be(3); - boundProject.Profiles[Language.VBNET].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[Language.CSharp].ProfileKey.Should().NotBeNull(); - boundProject.Profiles[FakeRoslynLanguage.Instance].ProfileKey.Should().BeNull(); - } - - [TestMethod] - public async Task UpdateAsync_SavesConfiguration() - { - // Arrange - var boundProject = CreateBoundProject(); - var configPersister = new DummyConfigPersister(); - - const string myProfileKey = "my profile key"; - var language = Language.VBNET; - var serverQpTimestamp = DateTime.UtcNow.AddHours(-2); - var qp = CreateQualityProfile(myProfileKey, serverQpTimestamp); - - SetupLanguagesToUpdate(out var outOfDateQualityProfileFinderMock, - boundProject, - (language, qp)); - - var configProviderMock = new Mock(); - configProviderMock.Setup(x => x.SaveConfigurationAsync(qp, - language, - It.IsAny(), CancellationToken.None)) - .Returns(Task.CompletedTask); - - var testSubject = CreateTestSubject( - outOfDateQualityProfileFinderMock.Object, - configProviderMock.Object, - configurationPersister: configPersister, - languageProvider: CreateLanguageProvider([language]).Object); - - // Act - await testSubject.UpdateAsync(boundProject, null, CancellationToken.None); - - // Assert - configPersister.SavedProject.Should().NotBeNull(); - - var savedProject = configPersister.SavedProject; - savedProject.ServerConnection.Id.Should().Be(boundProject.ServerConnection.Id); - savedProject.Profiles.Should().HaveCount(1); - savedProject.Profiles[Language.VBNET].ProfileKey.Should().Be(myProfileKey); - savedProject.Profiles[Language.VBNET].ProfileTimestamp.Should().Be(serverQpTimestamp); - } - - #region Helpers - - private static RoslynQualityProfileDownloader CreateTestSubject( - IOutOfDateQualityProfileFinder outOfDateQualityProfileFinder = null, - IBindingConfigProvider bindingConfigProvider = null, - DummyConfigPersister configurationPersister = null, - ILogger logger = null, - ILanguageProvider languageProvider = null) => - new( - bindingConfigProvider ?? Mock.Of(), - configurationPersister ?? new DummyConfigPersister(), - outOfDateQualityProfileFinder ?? Mock.Of(), - logger ?? new TestLogger(logToConsole: true), - languageProvider ?? CreateLanguageProvider().Object); - - private static SonarQubeQualityProfile CreateQualityProfile(string key = "key", DateTime timestamp = default) => new(key, default, default, default, timestamp); - - private static void SetupLanguagesToUpdate( - out Mock outOfDateQualityProfileFinderMock, - BoundServerProject boundProject, - params Language[] languages) => - SetupLanguagesToUpdate(out outOfDateQualityProfileFinderMock, - boundProject, - languages.Select(x => (x, CreateQualityProfile())).ToArray()); - - private static void SetupLanguagesToUpdate( - out Mock outOfDateQualityProfileFinderMock, - BoundServerProject boundProject, - params (Language language, SonarQubeQualityProfile qualityProfile)[] qps) - { - outOfDateQualityProfileFinderMock = new Mock(); - outOfDateQualityProfileFinderMock - .Setup(x => - x.GetAsync(boundProject, It.IsAny())) - .ReturnsAsync(qps); - } - - private static void SetupConfigSave( - Mock bindingConfigProvider, - RoslynLanguage language) - { - bindingConfigProvider.Setup(x => x.SaveConfigurationAsync( - It.IsAny(), - language, - It.IsAny(), - CancellationToken.None)) - .Returns(Task.CompletedTask); - } - - private static BoundServerProject CreateBoundProject( - string projectKey = "key", - Uri uri = null) => - new BoundServerProject( - "solution", - projectKey, - new ServerConnection.SonarQube(uri ?? new Uri("http://localhost/"))); - - private static void CheckRuleConfigSaved(Mock bindingConfig, RoslynLanguage language) => - bindingConfig.Verify( - x => - x.SaveConfigurationAsync( - It.IsAny(), - language, - It.IsAny(), - It.IsAny()), - Times.Once); - - private static void CheckRuleConfigNotSaved(Mock bindingConfig, RoslynLanguage language) => - bindingConfig.Verify( - x => - x.SaveConfigurationAsync( - It.IsAny(), - language, - It.IsAny(), - It.IsAny()), - Times.Never); - - private class DummyConfigPersister : IConfigurationPersister - { - public BoundServerProject SavedProject { get; private set; } - - BindingConfiguration IConfigurationPersister.Persist(BoundServerProject project) - { - SavedProject = project; - return new BindingConfiguration(project, SonarLintMode.Connected, "c:\\any"); - } - } - - private static Mock CreateLanguageProvider(RoslynLanguage[] languagesToBind = null) - { - var mockLanguageProvider = new Mock(); - mockLanguageProvider.Setup(x => x.RoslynLanguages) - .Returns(languagesToBind ?? LanguageProvider.Instance.RoslynLanguages.ToArray()); - return mockLanguageProvider; - } - - #endregion Helpers -} diff --git a/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs b/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs deleted file mode 100644 index 48d2f15c2a..0000000000 --- a/src/ConnectedMode.UnitTests/ServerIssueFinderTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; - -[TestClass] -public class ServerIssueFinderTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void FindServerIssueAsync_UIThread_Throws() - { - var serverIssueFinder = CreateTestSubject(out _, out _, out _, out _, out _, out var threadHandlingMock); - var exception = new Exception(); - threadHandlingMock.Setup(x => x.ThrowIfOnUIThread()).Throws(exception); - - Func act = async () => await serverIssueFinder.FindServerIssueAsync(Mock.Of(), CancellationToken.None); - - act.Should().Throw().Which.Should().BeSameAs(exception); - } - - [TestMethod] - public async Task FindServerIssueAsync_StandaloneMode_ReturnsNull() - { - var serverIssueFinder = CreateTestSubject(out _, out _, out var activeSolutionBoundTrackerMock, out _, out _, out _); - SetUpStandalone(activeSolutionBoundTrackerMock); - - var result = await serverIssueFinder.FindServerIssueAsync(Mock.Of(), CancellationToken.None); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task FindServerIssueAsync_RootCantBeCalculated_ReturnsNull() - { - const string filePath = @"c:\a\b\c"; - - var serverIssueFinder = CreateTestSubject(out var projectRootCalculatorMock, out _, out var activeSolutionBoundTrackerMock, out _, out _, out _); - SetUpBinding(activeSolutionBoundTrackerMock, "project"); - projectRootCalculatorMock.Setup(x => x.CalculateBasedOnLocalPathAsync(filePath, It.IsAny())) - .ReturnsAsync((string)null); - - var result = await serverIssueFinder.FindServerIssueAsync(CreateIssue("rule", filePath), CancellationToken.None); - - result.Should().BeNull(); - } - - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task FindServerIssueAsync_ReturnsMatchingServerIssueIfMatched(bool isMatch) - { - const string ruleId = "rule"; - const string filePath = @"c:\a\b\c"; - const string root = @"c:\a\"; - const string projectKey = "project"; - const string branch = "branch123"; - var localIssue = CreateIssue(ruleId, filePath); - var serverIssues = new[] { CreateServerIssue(), CreateServerIssue() }; - - var serverIssueFinder = CreateTestSubject(out var projectRootCalculatorMock, - out var issueMatcherMock, - out var activeSolutionBoundTrackerMock, - out var statefulServerBranchProviderMock, - out var sonarQubeServiceMock, - out _); - SetUpBinding(activeSolutionBoundTrackerMock, projectKey); - projectRootCalculatorMock - .Setup(x => x.CalculateBasedOnLocalPathAsync(filePath, It.IsAny())) - .ReturnsAsync(root); - statefulServerBranchProviderMock - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(branch); - sonarQubeServiceMock - .Setup(x => x.GetIssuesForComponentAsync(projectKey, branch, ComponentKeyGenerator.GetComponentKey(filePath, root, projectKey), ruleId, It.IsAny())) - .ReturnsAsync(serverIssues); - issueMatcherMock - .Setup(x => x.GetFirstLikelyMatchFromSameFileOrNull(localIssue, serverIssues)) - .Returns(isMatch ? serverIssues[1] : null); - - var result = await serverIssueFinder.FindServerIssueAsync(localIssue, CancellationToken.None); - - if (isMatch) - { - result.Should().BeSameAs(serverIssues[1]); - } - else - { - result.Should().BeNull(); - } - } - - private SonarQubeIssue CreateServerIssue() - { - return new SonarQubeIssue("test", "test", "test", "test", "test", "test", false, SonarQubeIssueSeverity.Info, - DateTimeOffset.MinValue, DateTimeOffset.MinValue, null, null); - } - - private IFilterableIssue CreateIssue(string ruleId, string filePath, int? startLine = null, string lineHash = null) => - new TestFilterableIssue - { - RuleId = ruleId, - FilePath = filePath, - StartLine = startLine, - LineHash = lineHash - }; - - private void SetUpStandalone(Mock activeSolutionBoundTrackerMock) => - SetUpBinding(activeSolutionBoundTrackerMock, null); - - private void SetUpBinding(Mock activeSolutionBoundTrackerMock, string projectKey) - { - activeSolutionBoundTrackerMock.SetupGet(x => x.CurrentConfiguration) - .Returns(projectKey == null - ? BindingConfiguration.Standalone - : new BindingConfiguration(new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), SonarLintMode.Connected, default)); - } - - private ServerIssueFinder CreateTestSubject(out Mock projectRootCalculatorMock, - out Mock issueMatcherMock, - out Mock activeSolutionBoundTrackerMock, - out Mock statefulServerBranchProviderMock, - out Mock sonarQubeServiceMock, - out Mock threadHandlingMock) - { - return new ServerIssueFinder((projectRootCalculatorMock = new Mock(MockBehavior.Strict)).Object, - (issueMatcherMock = new Mock(MockBehavior.Strict)).Object, - (activeSolutionBoundTrackerMock = new Mock(MockBehavior.Strict)).Object, - (statefulServerBranchProviderMock = new Mock(MockBehavior.Strict)).Object, - (sonarQubeServiceMock = new Mock(MockBehavior.Strict)).Object, - (threadHandlingMock = new Mock()).Object); // not strict since does not affect logic - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs deleted file mode 100644 index 0c61b698e2..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/Issue/IssueServerEventsListenerTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents.Issue -{ - [TestClass] - public class IssueServerEventsListenerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public async Task OnEvent_SingleEvent_MultipleTuples_StoreIsUpdatedWithIssuesFromCurrentBranch() - { - var event1 = CreateServerEvent(isResolved: true, - new BranchAndIssueKey("issueKey1", "branch1"), - new BranchAndIssueKey("issueKey2", "branch2"), - new BranchAndIssueKey("issueKey3", "branch1")); - - var issueServerEventSource = SetupIssueServerEventSource(event1); - var suppressionsUpdater = new Mock(); - var branchProvider = CreateBranchProvider("branch1"); - - var testSubject = CreateTestSubject(issueServerEventSource.Object, suppressionsUpdater.Object, branchProvider.Object); - - await testSubject.ListenAsync(); - - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Exactly(2)); - issueServerEventSource.VerifyNoOtherCalls(); - - suppressionsUpdater.Verify(x => x.UpdateSuppressedIssuesAsync( - true, - new[] { "issueKey1", "issueKey3" }, - It.IsAny()), - Times.Once); - suppressionsUpdater.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task OnEvent_MultipleEvents_StoreIsUpdatedWithIssuesFromCurrentBranch() - { - var event1 = CreateServerEvent(isResolved: true, new BranchAndIssueKey("issueKey1", "branch1")); - var event2 = CreateServerEvent(isResolved: true, new BranchAndIssueKey("issueKey2", "branch2")); - var event3 = CreateServerEvent(isResolved: false, new BranchAndIssueKey("issueKey3", "branch1")); - - var issueServerEventSource = SetupIssueServerEventSource(event1, event2, event3); - var suppressionUpdater = new Mock(); - var branchProvider = CreateBranchProvider("branch1"); - - var testSubject = CreateTestSubject(issueServerEventSource.Object, suppressionUpdater.Object, branchProvider.Object); - - await testSubject.ListenAsync(); - - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Exactly(4)); - issueServerEventSource.VerifyNoOtherCalls(); - - suppressionUpdater.Verify(x => x.UpdateSuppressedIssuesAsync( - true, - new[] { "issueKey1" }, - It.IsAny()), - Times.Once); - - suppressionUpdater.Verify(x => x.UpdateSuppressedIssuesAsync( - false, - new[] { "issueKey3" }, - It.IsAny()), - Times.Once); - - suppressionUpdater.VerifyNoOtherCalls(); - } - - [TestMethod] - public void OnEvent_FailureToProcessIssueEvent_CriticalException_StopsListeningToServerEventsSource() - { - var issueServerEventSource = new Mock(); - issueServerEventSource - .Setup(x => x.GetNextEventOrNullAsync()) - .Throws(new StackOverflowException("this is a test")); - - var testSubject = CreateTestSubject(issueServerEventSource: issueServerEventSource.Object); - - Func func = async () => await testSubject.ListenAsync(); - - func.Should().Throw().And.Message.Should().Be("this is a test"); - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); - issueServerEventSource.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Dispose_StopsListeningToServerEventsSource() - { - var issueServerEventSource = SetupIssueServerEventSource(); - var testSubject = CreateTestSubject(issueServerEventSource: issueServerEventSource.Object); - - var listenTask = testSubject.ListenAsync(); - testSubject.Dispose(); - - await listenTask; - - issueServerEventSource.Verify(x => x.GetNextEventOrNullAsync(), Times.Once); - } - - [TestMethod] - [Description("Regression test for https://github.com/SonarSource/sonarlint-visualstudio/issues/3946")] - public void Dispose_CalledASecondTime_NoException() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - - Action act = () => testSubject.Dispose(); - act.Should().NotThrow(); - } - - private static Mock SetupIssueServerEventSource(params IIssueChangedServerEvent[] serverEvents) - { - var issueServerEventSource = new Mock(); - - var sequence = issueServerEventSource.SetupSequence(x => x.GetNextEventOrNullAsync()); - - foreach (var serverEvent in serverEvents) - { - sequence.ReturnsAsync(serverEvent); - } - - // Signal that the task is finished - sequence.ReturnsAsync((IIssueChangedServerEvent)null); - - return issueServerEventSource; - } - - private static IssueServerEventsListener CreateTestSubject( - IIssueServerEventSource issueServerEventSource = null, - IRoslynSuppressionUpdater roslynSuppressionUpdater = null, - IStatefulServerBranchProvider branchProvider = null, - IThreadHandling threadHandling = null, - ILogger logger = null) - { - issueServerEventSource ??= Mock.Of(); - roslynSuppressionUpdater ??= Mock.Of(); - branchProvider ??= Mock.Of(); - threadHandling ??= new NoOpThreadHandler(); - logger ??= Mock.Of(); - - return new IssueServerEventsListener(issueServerEventSource, roslynSuppressionUpdater, branchProvider, threadHandling, logger); - } - - private static IIssueChangedServerEvent CreateServerEvent(bool isResolved, params BranchAndIssueKey[] branchAndIssueKeys) - { - var serverEvent = new Mock(); - serverEvent.Setup(x => x.IsResolved).Returns(isResolved); - serverEvent.Setup(x => x.BranchAndIssueKeys).Returns(branchAndIssueKeys); - - return serverEvent.Object; - } - - private Mock CreateBranchProvider(string serverBranch) - { - var branchProvider = new Mock(); - - branchProvider - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(serverBranch); - - return branchProvider; - } - } -} diff --git a/src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs b/src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs deleted file mode 100644 index e1d5e5cb41..0000000000 --- a/src/ConnectedMode.UnitTests/Suppressions/RoslynSuppressionUpdaterTests.cs +++ /dev/null @@ -1,391 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Suppressions; - -[TestClass] -public class RoslynSuppressionUpdaterTests -{ - private ICancellableActionRunner actionRunner; - private ILogger logger; - private IServerQueryInfoProvider queryInfo; - private ISonarQubeService server; - private RoslynSuppressionUpdater testSubject; - private IThreadHandling threadHandling; - private readonly EventHandler suppressedIssuesReloaded = Substitute.For>(); - private readonly EventHandler newIssuesSuppressed = Substitute.For>(); - private readonly EventHandler suppressionsRemoved = Substitute.For>(); - - [TestInitialize] - public void TestInitialize() - { - server = Substitute.For(); - queryInfo = Substitute.For(); - logger = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); - threadHandling = new NoOpThreadHandler(); - actionRunner = new SynchronizedCancellableActionRunner(logger); - testSubject = CreateTestSubject(actionRunner, threadHandling); - testSubject.SuppressedIssuesReloaded += suppressedIssuesReloaded; - testSubject.NewIssuesSuppressed += newIssuesSuppressed; - testSubject.SuppressionsRemoved += suppressionsRemoved; - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void Ctor_SetsLoggerContext() => logger.Received(1).ForContext(Resources.ConnectedModeLogContext, Resources.RoslynSuppressionsLogContext); - - [TestMethod] - [DataRow(null, null)] - [DataRow(null, "branch")] - [DataRow("projectKey", null)] - public async Task UpdateAll_MissingServerQueryInfo_DoesNotInvokeEvent(string projectKey, string branchName) - { - // Server query info is not available -> give up - MockQueryInfoProvider(projectKey, branchName); - - await testSubject.UpdateAllServerSuppressionsAsync(); - - await queryInfo.Received(1).GetProjectKeyAndBranchAsync(Arg.Any()); - server.ReceivedCalls().Should().HaveCount(0); - VerifySuppressedIssuesReloadedNotInvoked(); - } - - [TestMethod] - public async Task UpdateAll_HasServerQueryInfo_ServerQueriedAndEventInvoked() - { - // Happy path - fetch and update - MockQueryInfoProvider("project", "branch"); - var issue = CreateIssue("issue1"); - MockSonarQubeService("project", "branch", null, issue); - - await testSubject.UpdateAllServerSuppressionsAsync(); - - await queryInfo.Received(1).GetProjectKeyAndBranchAsync(Arg.Any()); - await server.Received(1).GetSuppressedRoslynIssuesAsync("project", "branch", null, Arg.Any()); - server.ReceivedCalls().Should().HaveCount(1); - VerifySuppressedIssuesReloadedInvoked(expectedAllSuppressedIssues: [issue]); - } - - [TestMethod] - public async Task UpdateAll_RunOnBackgroundThreadInActionRunner() - { - var mockedActionRunner = CreateMockedActionRunner(); - var mockedThreadHandling = CreateMockedThreadHandling(); - var mockedTestSubject = CreateTestSubject(mockedActionRunner, mockedThreadHandling); - - await mockedTestSubject.UpdateAllServerSuppressionsAsync(); - - Received.InOrder(() => - { - mockedThreadHandling.RunOnBackgroundThread(Arg.Any>>>()); - mockedActionRunner.RunAsync(Arg.Any>()); - queryInfo.GetProjectKeyAndBranchAsync(Arg.Any()); - }); - queryInfo.ReceivedCalls().Should().HaveCount(1); // no other calls - } - - [TestMethod] - public void UpdateAll_CriticalExpression_NotHandled() - { - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new StackOverflowException("thrown in a test")); - - var operation = testSubject.UpdateAllServerSuppressionsAsync; - - operation.Should().Throw().And.Message.Should().Be("thrown in a test"); - AssertMessageArgsDoesNotExist("thrown in a test"); - } - - [TestMethod] - public void UpdateAll_NonCriticalExpression_IsSuppressed() - { - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new InvalidOperationException("thrown in a test")); - - var operation = testSubject.UpdateAllServerSuppressionsAsync; - - operation.Should().NotThrow(); - AssertMessageArgsExists("thrown in a test"); - } - - [TestMethod] - public void UpdateAll_OperationCancelledException_CancellationMessageLogged() - { - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new OperationCanceledException("thrown in a test")); - - var operation = testSubject.UpdateAllServerSuppressionsAsync; - - operation.Should().NotThrow(); - AssertMessageArgsDoesNotExist("thrown in a test"); - AssertMessageExists(Resources.Suppressions_FetchOperationCancelled); - } - - [TestMethod] - [Ignore] // Flaky. See https://github.com/SonarSource/sonarlint-visualstudio/issues/4307 - public async Task UpdateAll_CallInProgress_CallIsCancelled() - { - var testTimeout = GetThreadedTestTimeout(); - - var signalToBlockFirstCall = new ManualResetEvent(false); - var signalToBlockSecondCall = new ManualResetEvent(false); - - var isFirstCall = true; - - queryInfo.GetProjectKeyAndBranchAsync(Arg.Any()) - .Returns(("projectKey", "branch")); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())) - .Do(x => - { - if (isFirstCall) - { - Log("[1] In first call"); - isFirstCall = false; - - Log("[1] Unblocking second call..."); - signalToBlockSecondCall.Set(); - - Log("[1] Waiting to be unblocked..."); - signalToBlockFirstCall.WaitOne(testTimeout); - - Log("[1] First call finished"); - } - else - { - Log("[2] In second call"); - - Log("[2] Unblocking the first call..."); - signalToBlockFirstCall.Set(); - Log("[2] Second call finished"); - } - }); - - var mockedTestSubject = CreateTestSubject( - // Note: need a real thread-handling implementation here as the test - // needs multiple threads. - actionRunner, ThreadHandling.Instance); - - // 1. First call - don't wait for it to finish, since it should be blocked - mockedTestSubject.UpdateAllServerSuppressionsAsync().Forget(); - - Log("[main test] Waiting to be unblocked..."); - signalToBlockSecondCall.WaitOne(testTimeout) - .Should().BeTrue(); // not expecting the test to timeout - - // 2. Second call - "await" means we're waiting for it to run to completion - await mockedTestSubject.UpdateAllServerSuppressionsAsync(); - - queryInfo.ReceivedCalls().Should().HaveCount(2); - var call1Token = (CancellationToken)queryInfo.ReceivedCalls().ToList()[0].GetArguments()[0]; - var call2Token = (CancellationToken)queryInfo.ReceivedCalls().ToList()[1].GetArguments()[0]; - - call1Token.IsCancellationRequested.Should().BeTrue(); - call2Token.IsCancellationRequested.Should().BeFalse(); - - AssertMessageExists(Resources.Suppressions_FetchOperationCancelled); - - // If cancellation worked then we should only have made one call to the SonarQubeService - server.ReceivedCalls().Should().HaveCount(1); - VerifySuppressedIssuesReloadedInvoked([]); - - static void Log(string message) => Console.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] {message}"); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_EmptyIssues_IssuesNotResolved_SuppressionsRemovedEventNotInvokedAndNoServerCalls() - { - await testSubject.UpdateSuppressedIssuesAsync(isResolved: false, [], CancellationToken.None); - - queryInfo.ReceivedCalls().Count().Should().Be(0); - server.ReceivedCalls().Count().Should().Be(0); - VerifySuppressionsRemovedNotInvoked(); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_IssuesNotResolved_IssuesFetched() - { - string[] issueKeys = ["issue1", "issue2"]; - - await testSubject.UpdateSuppressedIssuesAsync(isResolved: false, issueKeys, CancellationToken.None); - - VerifySuppressionsRemovedInvoked(issueKeys); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_EmptyIssues_NewIssuesSuppressedNotInvokedAndNoServerCalls() - { - MockQueryInfoProvider("proj1", "branch1"); - - await testSubject.UpdateSuppressedIssuesAsync(isResolved: true, [], CancellationToken.None); - - queryInfo.ReceivedCalls().Count().Should().Be(0); - server.ReceivedCalls().Count().Should().Be(0); - VerifyNewIssuesSuppressedNotInvoked(); - } - - [TestMethod] - public async Task UpdateSuppressedIssuesAsync_IssuesSuppressed_IssuesFetched() - { - MockQueryInfoProvider("proj1", "branch1"); - var expectedFetchedIssues = new[] { CreateIssue("issue1") }; - MockSonarQubeService( - "proj1", - "branch1", - ["issue1"], - expectedFetchedIssues); - - await testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], CancellationToken.None); - - await server.Received(1).GetSuppressedRoslynIssuesAsync( - "proj1", - "branch1", - Arg.Is(x => x.SequenceEqual(new[] { "issue1" })), - Arg.Any()); - VerifyNewIssuesSuppressedInvoked(expectedFetchedIssues); - AssertMessageExists(Resources.Suppressions_Fetch_Issues); - AssertMessageExists(Resources.Suppression_Fetch_Issues_Finished); - } - - [TestMethod] - public void UpdateSuppressedIssuesAsync_IssuesSuppressed_CriticalExpression_NotHandled() - { - MockQueryInfoProvider("proj1", "branch1"); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new StackOverflowException("thrown in a test")); - - var operation = () => testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], CancellationToken.None); - - operation.Should().Throw().And.Message.Should().Be("thrown in a test"); - AssertMessageArgsDoesNotExist("thrown in a test"); - VerifyNewIssuesSuppressedNotInvoked(); - } - - [TestMethod] - public void UpdateSuppressedIssuesAsync_IssuesSuppressed_NonCriticalExpression_IsSuppressed() - { - MockQueryInfoProvider("proj1", "branch1"); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Throw(new InvalidOperationException("thrown in a test")); - - var operation = () => testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], CancellationToken.None); - - operation.Should().NotThrow(); - AssertMessageArgsExists("thrown in a test"); - VerifyNewIssuesSuppressedNotInvoked(); - } - - [TestMethod] - public void UpdateSuppressedIssuesAsync_IssuesSuppressed_OperationCancelledException_CancellationMessageLogged() - { - MockQueryInfoProvider("proj1", "branch1"); - using var cancellationTokenSource = new CancellationTokenSource(); - queryInfo.When(x => x.GetProjectKeyAndBranchAsync(Arg.Any())).Do(_ => cancellationTokenSource.Cancel()); - - var operation = () => testSubject.UpdateSuppressedIssuesAsync(isResolved: true, ["issue1"], cancellationTokenSource.Token); - - operation.Should().NotThrow(); - AssertMessageExists(Resources.Suppressions_FetchOperationCancelled); - VerifyNewIssuesSuppressedNotInvoked(); - } - - private RoslynSuppressionUpdater CreateTestSubject(ICancellableActionRunner mockedActionRunner, IThreadHandling mockedThreadHandling) => - new(server, queryInfo, mockedActionRunner, logger, mockedThreadHandling); - - private void MockQueryInfoProvider(string projectKey, string branchName) => queryInfo.GetProjectKeyAndBranchAsync(Arg.Any()).Returns((projectKey, branchName)); - - private void MockSonarQubeService( - string projectKey, - string branchName, - string[] issueKeys, - params SonarQubeIssue[] issuesToReturn) => - server.GetSuppressedRoslynIssuesAsync(projectKey, branchName, Arg.Is(x => issueKeys == null || x.SequenceEqual(issueKeys)), Arg.Any()).Returns(issuesToReturn); - - private static SonarQubeIssue CreateIssue(string issueKey) => - new(issueKey, - null, null, null, null, null, true, - SonarQubeIssueSeverity.Info, DateTimeOffset.MinValue, - DateTimeOffset.MaxValue, null, null); - - private static TimeSpan GetThreadedTestTimeout() - // This test uses a number of manual signals to control the order of execution. - // We want a longer timeout when debugging. - => - Debugger.IsAttached ? TimeSpan.FromMinutes(2) : TimeSpan.FromMilliseconds(200); - - private static IThreadHandling CreateMockedThreadHandling() - { - var mockedThreadHandling = Substitute.For(); - mockedThreadHandling.When(x => x.RunOnBackgroundThread(Arg.Any>>>())).Do(x => - { - var func = x.Arg>>>(); - func(); - }); - return mockedThreadHandling; - } - - private static ICancellableActionRunner CreateMockedActionRunner() - { - var mockedActionRunner = Substitute.For(); - mockedActionRunner.When(x => x.RunAsync(Arg.Any>())) - .Do(callInfo => - { - var func = callInfo.Arg>(); - func(new CancellationToken()); - }); - return mockedActionRunner; - } - - private void VerifySuppressedIssuesReloadedNotInvoked() => suppressedIssuesReloaded.DidNotReceiveWithAnyArgs().Invoke(testSubject, Arg.Any()); - - private void VerifySuppressedIssuesReloadedInvoked(IEnumerable expectedAllSuppressedIssues) => - suppressedIssuesReloaded.Received(1) - .Invoke(testSubject, Arg.Is(x => x.SuppressedIssues.SequenceEqual(expectedAllSuppressedIssues))); - - private void VerifySuppressionsRemovedNotInvoked() => suppressionsRemoved.DidNotReceiveWithAnyArgs().Invoke(testSubject, Arg.Any()); - - private void VerifySuppressionsRemovedInvoked(IEnumerable expectedSuppressedIssuesKeys) => - suppressionsRemoved.Received(1) - .Invoke(testSubject, Arg.Is(x => x.IssueServerKeys.SequenceEqual(expectedSuppressedIssuesKeys))); - - private void VerifyNewIssuesSuppressedInvoked(IEnumerable expectedAllSuppressedIssues) => - newIssuesSuppressed.Received(1) - .Invoke(testSubject, Arg.Is(x => x.SuppressedIssues.SequenceEqual(expectedAllSuppressedIssues))); - - private void VerifyNewIssuesSuppressedNotInvoked() => newIssuesSuppressed.DidNotReceiveWithAnyArgs().Invoke(testSubject, Arg.Any()); - - private void AssertMessageArgsDoesNotExist(params object[] args) => logger.DidNotReceive().WriteLine(Arg.Any(), Arg.Is(x => x.SequenceEqual(args))); - - private void AssertMessageArgsExists(params object[] args) => logger.Received(1).WriteLine(Arg.Any(), Arg.Is(x => x.SequenceEqual(args))); - - private void AssertMessageExists(string message) => logger.Received(1).WriteLine(Arg.Is(x => x == message), Arg.Any()); -} diff --git a/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs b/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs deleted file mode 100644 index ea9b2f4adb..0000000000 --- a/src/ConnectedMode.UnitTests/Suppressions/TimedUpdateHandlerTests.cs +++ /dev/null @@ -1,179 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Suppressions -{ - [TestClass] - public class TimedUpdateHandlerTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_TimerIsSetupAsExpected() - { - var refreshTimer = new Mock(); - refreshTimer.SetupProperty(x => x.AutoReset); - refreshTimer.SetupProperty(x => x.Interval); - - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Connected); - - _ = CreateTestSubject(activeSolutionBoundTracker: activeSolutionBoundTracker.Object, timerFactory: timerFactory); - - refreshTimer.Object.AutoReset.Should().BeTrue(); - refreshTimer.Object.Interval.Should().Be(1000 * 60 * 10); - refreshTimer.VerifyAdd(x => x.Elapsed += It.IsAny>(), Times.Once); - refreshTimer.Verify(x => x.Start(), Times.Once); - } - - [TestMethod] - [DataRow(SonarLintMode.Standalone, false)] - [DataRow(SonarLintMode.Connected, true)] - [DataRow(SonarLintMode.LegacyConnected, true)] - public void Ctor_DependingOnBindingConfig_InitialTimeStateSetCorrectly(SonarLintMode mode, bool start) - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(mode); - - _ = CreateTestSubject(activeSolutionBoundTracker: activeSolutionBoundTracker.Object, timerFactory: timerFactory); - - refreshTimer.Verify(x => x.Start(), start ? Times.Once : Times.Never); - refreshTimer.Verify(x => x.Stop(), start ? Times.Never : Times.Once); - } - - [TestMethod] - public void InvokeEvent_TimerElapsed_StoreUpdatersAreCalled() - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Connected); - var suppressionUpdater = new Mock(); - var qualityProfileUpdater = new Mock(); - - _ = CreateTestSubject(activeSolutionBoundTracker.Object, suppressionUpdater.Object, qualityProfileUpdater.Object, timerFactory); - - refreshTimer.Raise(x => x.Elapsed += null, new TimerEventArgs(DateTime.UtcNow)); - - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Once); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Once); - } - - [TestMethod] - public void Dispose_RefreshTimerDisposed_RaisingEventDoesNothing() - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Connected); - var suppressionUpdater = new Mock(); - var qualityProfileUpdater = new Mock(); - - var testSubject = CreateTestSubject(activeSolutionBoundTracker.Object, suppressionUpdater.Object, qualityProfileUpdater.Object, timerFactory); - - testSubject.Dispose(); - - refreshTimer.Verify(x => x.Dispose(), Times.Once); - refreshTimer.Raise(x => x.Elapsed += null, new TimerEventArgs(DateTime.UtcNow)); - - suppressionUpdater.Verify(x => x.UpdateAllServerSuppressionsAsync(), Times.Never); - qualityProfileUpdater.Verify(x => x.UpdateAsync(), Times.Never); - } - - [TestMethod] - public void InvokeBindingChanged_TimerStartsStopsOnActiveSolutionBoundChange() - { - var refreshTimer = new Mock(); - var timerFactory = CreateTimerFactory(refreshTimer.Object); - var activeSolutionBoundTracker = CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode.Standalone); - - _ = CreateTestSubject(activeSolutionBoundTracker: activeSolutionBoundTracker.Object, timerFactory: timerFactory); - - refreshTimer.Reset(); - - activeSolutionBoundTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(CreateBindingConfiguration(SonarLintMode.Connected))); - refreshTimer.Verify(x => x.Start(), Times.Once); - refreshTimer.Verify(x => x.Stop(), Times.Never); - - activeSolutionBoundTracker.Raise(x => x.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(CreateBindingConfiguration(SonarLintMode.Standalone))); - refreshTimer.Verify(x => x.Start(), Times.Once); - refreshTimer.Verify(x => x.Stop(), Times.Once); - } - - private static ITimerFactory CreateTimerFactory(ITimer timer) - { - var timerFactory = new Mock(); - - timerFactory.Setup(x => x.Create()).Returns(timer); - - return timerFactory.Object; - } - - private static TimedUpdateHandler CreateTestSubject( - IActiveSolutionBoundTracker activeSolutionBoundTracker, - IRoslynSuppressionUpdater roslynSuppressionUpdater = null, - IQualityProfileUpdater qualityProfileUpdater = null, - ITimerFactory timerFactory = null) - { - return new TimedUpdateHandler( - roslynSuppressionUpdater ?? Mock.Of(), - qualityProfileUpdater ?? Mock.Of(), - activeSolutionBoundTracker, - new TestLogger(logToConsole: true), - timerFactory ?? Mock.Of()); - } - - private BindingConfiguration CreateBindingConfiguration(SonarLintMode mode) - { - return new BindingConfiguration(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://localhost"))), mode, ""); - } - - private Mock CreateActiveSolutionBoundTrackerWihtBindingConfig(SonarLintMode mode) - { - var activeSolutionTracker = new Mock(); - - var bindingConfig = CreateBindingConfiguration(mode); - - activeSolutionTracker.Setup(x => x.CurrentConfiguration).Returns(bindingConfig); - - return activeSolutionTracker; - } - } -} diff --git a/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs b/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs index 92e18ebbdc..8589e148f5 100644 --- a/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs +++ b/src/ConnectedMode.UnitTests/Transition/MuteIssuesServiceTests.cs @@ -20,12 +20,10 @@ using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.ConnectedMode.Transition; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Suppressions; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.SLCore; using SonarLint.VisualStudio.SLCore.Core; @@ -40,9 +38,7 @@ namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Transition; public class MuteIssuesServiceTests { private const string AnIssueServerKey = "issueServerKey"; - private const string RoslynIssueServerKey = "roslynKey"; private readonly IAnalysisIssueVisualization nonRoslynIssue = Substitute.For(); - private readonly IFilterableRoslynIssue roslynIssue = Substitute.For(); private MuteIssuesService testSubject; private IMuteIssuesWindowService muteIssuesWindowService; @@ -51,8 +47,6 @@ public class MuteIssuesServiceTests private TestLogger logger; private IThreadHandling threadHandling; private IIssueSLCoreService issueSlCoreService; - private IServerIssueFinder serverIssueFinder; - private IRoslynSuppressionUpdater roslynSuppressionUpdater; [TestInitialize] public void TestInitialize() @@ -60,12 +54,10 @@ public void TestInitialize() muteIssuesWindowService = Substitute.For(); activeConfigScopeTracker = Substitute.For(); slCoreServiceProvider = Substitute.For(); - serverIssueFinder = Substitute.For(); - roslynSuppressionUpdater = Substitute.For(); logger = new TestLogger(); threadHandling = new NoOpThreadHandler(); issueSlCoreService = Substitute.For(); - testSubject = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, serverIssueFinder, roslynSuppressionUpdater, logger, + testSubject = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, logger, threadHandling); MockNonRoslynIssue(); @@ -83,8 +75,6 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -96,7 +86,7 @@ public void Logger_HasCorrectContext() { var substituteLogger = Substitute.For(); - _ = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, serverIssueFinder, roslynSuppressionUpdater, substituteLogger, + _ = new MuteIssuesService(muteIssuesWindowService, activeConfigScopeTracker, slCoreServiceProvider, substituteLogger, threadHandling); substituteLogger.Received(1).ForContext("MuteIssuesService"); @@ -115,31 +105,6 @@ public void ResolveIssueWithDialogAsync_WhenIssueServerKeyIsNull_LogsAndThrows(s logger.AssertPartialOutputStrings(Resources.MuteIssue_IssueNotFound); } - [TestMethod] - [DataRow(null)] - [DataRow("")] - public void ResolveIssueWithDialogAsync_WhenRoslynIssueServerKeyIsNull_LogsAndThrows(string issueServerKey) - { - MockRoslynIssueOnServer(issueServerKey); - - var act = () => testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - act.Should().Throw().WithMessage(Resources.MuteIssue_IssueNotFound); - logger.AssertPartialOutputStrings(Resources.MuteIssue_IssueNotFound); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_WhenRoslynIssueServerIsAlreadyResolvedOnServer_LogsAndThrows() - { - var serverIssue = MockRoslynIssueOnServer(RoslynIssueServerKey); - serverIssue.IsResolved = true; - - var act = () => testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - act.Should().Throw().WithMessage(Resources.MuteIssue_ErrorIssueAlreadyResolved); - logger.AssertPartialOutputStrings(Resources.MuteIssue_ErrorIssueAlreadyResolved); - } - [TestMethod] public void ResolveIssueWithDialogAsync_WhenConfigScopeIsNotSet_LogsAndThrows() { @@ -237,25 +202,6 @@ public void ResolveIssueWithDialogAsync_WhenWindowResponseResultIsTrue_ShouldMut && !x.isTaintIssue)); } - [TestMethod] - [DataRow(ResolutionStatus.ACCEPT, SonarQubeIssueTransition.Accept)] - [DataRow(ResolutionStatus.WONT_FIX, SonarQubeIssueTransition.WontFix)] - [DataRow(ResolutionStatus.FALSE_POSITIVE, SonarQubeIssueTransition.FalsePositive)] - public void ResolveIssueWithDialogAsync_WhenWindowResponseResultIsTrue_AndRoslynIssue_ShouldMuteIssue(ResolutionStatus resolutionStatus, SonarQubeIssueTransition transition) - { - MuteIssuePermitted(); - MockRoslynIssueOnServer(RoslynIssueServerKey); - muteIssuesWindowService.Show(Arg.Any>()).Returns(new MuteIssuesWindowResponse { Result = true, IssueTransition = transition }); - - _ = testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - issueSlCoreService.Received().ChangeStatusAsync(Arg.Is(x => - x.issueKey == RoslynIssueServerKey - && x.newStatus == resolutionStatus - && x.configurationScopeId == "CONFIG_SCOPE_ID" - && !x.isTaintIssue)); - } - [TestMethod] public void ResolveIssueWithDialogAsync_WhenWindowResponseHasComment_ShouldAddComment() { @@ -322,56 +268,6 @@ public void ResolveIssueWithDialogAsync_WhenMuteIssueFails_LogsAndThrows() logger.AssertPartialOutputStrings("Some error"); } - [TestMethod] - public void ResolveIssueWithDialogAsync_RoslynIssueMutedSuccessfully_CallsSuppressionsUpdater() - { - MuteIssuePermitted(); - MockRoslynIssueOnServer(RoslynIssueServerKey); - MockIssueAcceptedInWindow(); - - _ = testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - roslynSuppressionUpdater.Received(1).UpdateSuppressedIssuesAsync(isResolved: true, Arg.Is(x => x.SequenceEqual(new[] { RoslynIssueServerKey })), Arg.Any()); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_NonRoslynIssueMutedSuccessfully_DoesNotCallSuppressionsUpdater() - { - MuteIssuePermitted(); - MockIssueAcceptedInWindow(); - - _ = testSubject.ResolveIssueWithDialogAsync(nonRoslynIssue); - - roslynSuppressionUpdater.DidNotReceiveWithAnyArgs().UpdateSuppressedIssuesAsync(default, default, default); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_RoslynIssueMutedSuccessfullyButCommentFails_CallsSuppressionsUpdater() - { - MuteIssuePermitted(); - MockRoslynIssueOnServer(RoslynIssueServerKey); - const string comment = "No you are not an issue, you are a feature"; - MockIssueAcceptedInWindow(comment); - issueSlCoreService.AddCommentAsync(Arg.Any()).ThrowsAsync(new Exception("Some error")); - - _ = testSubject.ResolveIssueWithDialogAsync(roslynIssue); - - roslynSuppressionUpdater.Received(1).UpdateSuppressedIssuesAsync(isResolved: true, Arg.Is(x => x.SequenceEqual(new[] { RoslynIssueServerKey })), Arg.Any()); - } - - [TestMethod] - public void ResolveIssueWithDialogAsync_NonRoslynIssueMutedSuccessfullyButCommentFails_DoesCallSuppressionsUpdater() - { - MuteIssuePermitted(); - const string comment = "No you are not an issue, you are a feature"; - MockIssueAcceptedInWindow(comment); - issueSlCoreService.AddCommentAsync(Arg.Any()).ThrowsAsync(new Exception("Some error")); - - _ = testSubject.ResolveIssueWithDialogAsync(nonRoslynIssue); - - roslynSuppressionUpdater.DidNotReceive().UpdateSuppressedIssuesAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - private void NotInConnectedMode() => activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope("CONFIG_SCOPE_ID")); private void ServiceProviderNotInitialized() => slCoreServiceProvider.TryGetTransientService(out Arg.Any()).ReturnsForAnyArgs(false); @@ -390,6 +286,10 @@ private void AssertMuteIssueWithoutComment() issueSlCoreService.DidNotReceiveWithAnyArgs().AddCommentAsync(Arg.Any()); } + private void MockIssueAcceptedInWindow(string comment = null) => + muteIssuesWindowService.Show(Arg.Any>()) + .Returns(new MuteIssuesWindowResponse { Result = true, IssueTransition = SonarQubeIssueTransition.Accept, Comment = comment }); + private void MockNonRoslynIssue() { var analysisBase = Substitute.For(); @@ -397,19 +297,4 @@ private void MockNonRoslynIssue() nonRoslynIssue.Issue.Returns(analysisBase); nonRoslynIssue.FilePath.Returns("C:\\somePath.cs"); } - - private SonarQubeIssue MockRoslynIssueOnServer(string issueServerKey) - { - roslynIssue.FilePath.Returns("C:\\someOtherPath.cs"); - var serverIssue = CreateServerIssue(issueServerKey); - serverIssueFinder.FindServerIssueAsync(roslynIssue, Arg.Any()).Returns(serverIssue); - return serverIssue; - } - - public static SonarQubeIssue CreateServerIssue(string issueKey, bool isResolved = false) => - new(issueKey, default, default, default, default, default, isResolved, default, default, default, default, default); - - private void MockIssueAcceptedInWindow(string comment = null) => - muteIssuesWindowService.Show(Arg.Any>()) - .Returns(new MuteIssuesWindowResponse { Result = true, IssueTransition = SonarQubeIssueTransition.Accept, Comment = comment }); } diff --git a/src/ConnectedMode/Binding/IBindingConfigProvider.cs b/src/ConnectedMode/Binding/IBindingConfigProvider.cs deleted file mode 100644 index e1740ee5f0..0000000000 --- a/src/ConnectedMode/Binding/IBindingConfigProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding; - -/// -/// Contract to provide the binding-related configuration for one or more languages -/// -public interface IBindingConfigProvider -{ - /// - /// Returns a configuration file for the specified language - /// - Task SaveConfigurationAsync(SonarQubeQualityProfile qualityProfile, Language language, - BindingConfiguration bindingConfiguration, CancellationToken cancellationToken); -} diff --git a/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs b/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs deleted file mode 100644 index 98b8702a68..0000000000 --- a/src/ConnectedMode/Binding/RoslynBindingConfigProvider.cs +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding; - -/// -/// Turn server Quality Profile information into a set of config files for a particular language -/// -[Export(typeof(IBindingConfigProvider))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class RoslynBindingConfigProvider( - ISonarQubeService sonarQubeService, - ILogger logger, - IRoslynConfigGenerator roslynConfigGenerator, - ILanguageProvider languageProvider) - : IBindingConfigProvider -{ - private const string TaintAnalysisRepoPrefix = "roslyn.sonaranalyzer.security."; - private readonly IEnvironmentSettings environmentSettings = new EnvironmentSettings(); - - public bool IsLanguageSupported(Language language) => languageProvider.RoslynLanguages.Contains(language); - - public Task SaveConfigurationAsync( - SonarQubeQualityProfile qualityProfile, - Language language, - BindingConfiguration bindingConfiguration, - CancellationToken cancellationToken) - { - if (!IsLanguageSupported(language)) - { - throw new ArgumentOutOfRangeException(nameof(language)); - } - - return SaveConfigurationInternalAsync(qualityProfile, language as RoslynLanguage, bindingConfiguration, cancellationToken); - } - - private async Task SaveConfigurationInternalAsync( - SonarQubeQualityProfile qualityProfile, - RoslynLanguage language, - BindingConfiguration bindingConfiguration, - CancellationToken cancellationToken) - { - var serverLanguageKey = language.ServerLanguageKey; - Debug.Assert(serverLanguageKey != null, - $"Server language should not be null for supported language: {language.Id}"); - - // First, fetch the active rules - var activeRules = (await FetchSupportedRulesAsync(true, qualityProfile.Key, cancellationToken)).ToList(); - - // Give up if the quality profile is empty - no point in fetching anything else - if (!activeRules.Any()) - { - logger.WriteLine(string.Format(BindingStrings.SubTextPaddingFormat, - string.Format(BindingStrings.NoSonarAnalyzerActiveRulesForQualityProfile, qualityProfile.Name, language.Name))); - // NOTE: this should never happen, binding config should be present for every supported language - throw new InvalidOperationException( - string.Format(QualityProfilesStrings.FailedToCreateBindingConfigForLanguage, language.Name)); - } - - // Now fetch the data required for the NuGet configuration - var sonarProperties = await FetchPropertiesAsync(bindingConfiguration.Project.ServerProjectKey, cancellationToken); - - // Finally, fetch the remaining data needed to build the globalconfig - var inactiveRules = await FetchSupportedRulesAsync(false, qualityProfile.Key, cancellationToken); - var exclusions = await FetchInclusionsExclusionsAsync(bindingConfiguration.Project.ServerProjectKey, cancellationToken); - - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - bindingConfiguration.BindingConfigDirectory, - sonarProperties, - exclusions, - activeRules.Union(inactiveRules).Select(x => new SonarQubeRoslynRuleStatus(x, environmentSettings)).ToList(), - activeRules); - } - - private async Task FetchInclusionsExclusionsAsync( - string projectKey, - CancellationToken cancellationToken) - { - var exclusions = await WebServiceHelper.SafeServiceCallAsync( - () => sonarQubeService.GetServerExclusions(projectKey, cancellationToken), logger); - - return exclusions; - } - - private async Task> FetchSupportedRulesAsync(bool active, string qpKey, CancellationToken cancellationToken) - { - var rules = await WebServiceHelper.SafeServiceCallAsync( - () => sonarQubeService.GetRulesAsync(active, qpKey, cancellationToken), logger); - return rules.Where(IsSupportedRule).ToArray(); - } - - private async Task> FetchPropertiesAsync(string projectKey, CancellationToken cancellationToken) - { - var serverProperties = await WebServiceHelper.SafeServiceCallAsync( - () => sonarQubeService.GetAllPropertiesAsync(projectKey, cancellationToken), logger); - - return serverProperties.ToDictionary(x => x.Key, x => x.Value); - } - - internal static /* for testing */ bool IsSupportedRule(SonarQubeRule rule) - { - // We don't want to generate configuration for taint-analysis rules or hotspots. - // * taint-analysis rules: these are in a separate analyzer that doesn't ship in SLVS so there is no point in generating config - // * hotspots: these are noisy so we don't want to run them in the IDE. There is special code in the Sonar hotspot analyzers to - // control when they run; we are responsible for not generating configuration for them. - return IsSupportedIssueType(rule.IssueType) && !IsTaintAnalysisRule(rule); - } - - private static bool IsTaintAnalysisRule(SonarQubeRule rule) => rule.RepositoryKey.StartsWith(TaintAnalysisRepoPrefix, StringComparison.OrdinalIgnoreCase); - - private static bool IsSupportedIssueType(SonarQubeIssueType issueType) => - issueType == SonarQubeIssueType.CodeSmell || - issueType == SonarQubeIssueType.Bug || - issueType == SonarQubeIssueType.Vulnerability; -} diff --git a/src/ConnectedMode/BoundSolutionUpdateHandler.cs b/src/ConnectedMode/BoundSolutionUpdateHandler.cs deleted file mode 100644 index bd643efe18..0000000000 --- a/src/ConnectedMode/BoundSolutionUpdateHandler.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - [Export(typeof(BoundSolutionUpdateHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class BoundSolutionUpdateHandler : IDisposable - { - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IQualityProfileUpdater qualityProfileUpdater; - - private bool disposed; - - [ImportingConstructor] - public BoundSolutionUpdateHandler( - IActiveSolutionBoundTracker activeSolutionBoundTracker, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IQualityProfileUpdater qualityProfileUpdater) - { - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.qualityProfileUpdater = qualityProfileUpdater; - - this.activeSolutionBoundTracker.SolutionBindingChanged += OnSolutionBindingChanged; - this.activeSolutionBoundTracker.SolutionBindingUpdated += OnSolutionBindingUpdated; - } - - private void OnSolutionBindingUpdated(object sender, EventArgs e) => TriggerUpdate(); - - private void OnSolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs e) => TriggerUpdate(); - - private void TriggerUpdate() - { - roslynSuppressionUpdater.UpdateAllServerSuppressionsAsync().Forget(); - qualityProfileUpdater.UpdateAsync().Forget(); - } - - public void Dispose() - { - if (!disposed) - { - activeSolutionBoundTracker.SolutionBindingChanged -= OnSolutionBindingChanged; - activeSolutionBoundTracker.SolutionBindingUpdated -= OnSolutionBindingUpdated; - disposed = true; - } - } - } -} diff --git a/src/ConnectedMode/ConnectedModePackage.cs b/src/ConnectedMode/ConnectedModePackage.cs index ad9970ba2b..53f31f7aae 100644 --- a/src/ConnectedMode/ConnectedModePackage.cs +++ b/src/ConnectedMode/ConnectedModePackage.cs @@ -25,9 +25,7 @@ using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.ConnectedMode.Hotspots; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using Task = System.Threading.Tasks.Task; @@ -41,10 +39,7 @@ namespace SonarLint.VisualStudio.ConnectedMode public sealed class ConnectedModePackage : AsyncPackage { private SSESessionManager sseSessionManager; - private IIssueServerEventsListener issueServerEventsListener; private IQualityProfileServerEventsListener qualityProfileServerEventsListener; - private BoundSolutionUpdateHandler boundSolutionUpdateHandler; - private TimedUpdateHandler timedUpdateHandler; private IHotspotDocumentClosedHandler hotspotDocumentClosedHandler; private IHotspotSolutionClosedHandler hotspotSolutionClosedHandler; private ILocalHotspotStoreMonitor hotspotStoreMonitor; @@ -60,15 +55,9 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke LoadServicesAndDoInitialUpdates(componentModel); - issueServerEventsListener = componentModel.GetService(); - issueServerEventsListener.ListenAsync().Forget(); - qualityProfileServerEventsListener = componentModel.GetService(); qualityProfileServerEventsListener.ListenAsync().Forget(); - boundSolutionUpdateHandler = componentModel.GetService(); - timedUpdateHandler = componentModel.GetService(); - hotspotDocumentClosedHandler = componentModel.GetService(); hotspotSolutionClosedHandler = componentModel.GetService(); @@ -88,8 +77,6 @@ private void LoadServicesAndDoInitialUpdates(IComponentModel componentModel) { sseSessionManager = componentModel.GetService(); sseSessionManager.CreateSessionIfInConnectedMode(); - var updater = componentModel.GetService(); - updater.UpdateAllServerSuppressionsAsync().Forget(); } protected override void Dispose(bool disposing) @@ -97,9 +84,6 @@ protected override void Dispose(bool disposing) if (disposing) { sseSessionManager?.Dispose(); - issueServerEventsListener?.Dispose(); - boundSolutionUpdateHandler?.Dispose(); - timedUpdateHandler?.Dispose(); } base.Dispose(disposing); diff --git a/src/ConnectedMode/Migration/ConnectedModeMigration.cs b/src/ConnectedMode/Migration/ConnectedModeMigration.cs index 12b67be23e..6b98538a15 100644 --- a/src/ConnectedMode/Migration/ConnectedModeMigration.cs +++ b/src/ConnectedMode/Migration/ConnectedModeMigration.cs @@ -21,7 +21,6 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.ConnectedMode.Binding; using SonarLint.VisualStudio.ConnectedMode.Shared; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client; @@ -44,7 +43,6 @@ private sealed class ChangedFiles : List> private readonly IVsAwareFileSystem fileSystem; private readonly ISonarQubeService sonarQubeService; private readonly IUnintrusiveBindingController unintrusiveBindingController; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; private readonly ISharedBindingConfigProvider sharedBindingConfigProvider; private readonly ILogger logger; private readonly IThreadHandling threadHandling; @@ -63,7 +61,6 @@ public ConnectedModeMigration( IVsAwareFileSystem fileSystem, ISonarQubeService sonarQubeService, IUnintrusiveBindingController unintrusiveBindingController, - IRoslynSuppressionUpdater roslynSuppressionUpdater, ISharedBindingConfigProvider sharedBindingConfigProvider, ILogger logger, IThreadHandling threadHandling, @@ -77,7 +74,6 @@ public ConnectedModeMigration( this.fileSystem = fileSystem; this.sonarQubeService = sonarQubeService; this.unintrusiveBindingController = unintrusiveBindingController; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; this.sharedBindingConfigProvider = sharedBindingConfigProvider; this.logger = logger; @@ -160,9 +156,6 @@ await unintrusiveBindingController.BindAsync(oldBinding.FromBoundSonarQubeProjec // i.e. update project files and delete .sonarlint folder await MakeLegacyFileChangesAsync(legacySettings, changedFiles, progress, token); - // Trigger a re-fetch of suppressions so the Roslyn settings are updated. - await roslynSuppressionUpdater.UpdateAllServerSuppressionsAsync(); - if (shareBinding) { progress?.Report(new MigrationProgress(0, 1, "Saving Shared Binding Config...", false)); diff --git a/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs b/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs deleted file mode 100644 index 30f6f2f9e6..0000000000 --- a/src/ConnectedMode/QualityProfiles/OutOfDateQualityProfileFinder.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles -{ - internal interface IOutOfDateQualityProfileFinder - { - /// - /// Gives the list of outdated quality profiles based on the existing ones from - /// - Task> GetAsync( - BoundServerProject sonarQubeProject, - CancellationToken cancellationToken); - } - - [Export(typeof(IOutOfDateQualityProfileFinder))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class OutOfDateQualityProfileFinder : IOutOfDateQualityProfileFinder - { - private readonly ISonarQubeService sonarQubeService; - private readonly ILanguageProvider languageProvider; - - [ImportingConstructor] - public OutOfDateQualityProfileFinder(ISonarQubeService sonarQubeService, ILanguageProvider languageProvider) - { - this.sonarQubeService = sonarQubeService; - this.languageProvider = languageProvider; - } - - public async Task> GetAsync( - BoundServerProject sonarQubeProject, - CancellationToken cancellationToken) - { - var sonarQubeQualityProfiles = - await sonarQubeService.GetAllQualityProfilesAsync(sonarQubeProject.ServerProjectKey, - (sonarQubeProject.ServerConnection as ServerConnection.SonarCloud)?.OrganizationKey, - cancellationToken); - - return sonarQubeQualityProfiles - .Select(serverQualityProfile => - (language: languageProvider.GetLanguageFromLanguageKey(serverQualityProfile.Language), - qualityProfile: serverQualityProfile)) - .Where(languageAndQp => - IsLocalQPOutOfDate(sonarQubeProject, languageAndQp.language, languageAndQp.qualityProfile)) - .ToArray(); - } - - private static bool IsLocalQPOutOfDate( - BoundServerProject sonarQubeProject, - Language language, - SonarQubeQualityProfile serverQualityProfile) - { - if (language == null || !sonarQubeProject.Profiles.TryGetValue(language, out var localQualityProfile)) - { - return false; - } - - return !serverQualityProfile.Key.Equals(localQualityProfile.ProfileKey) - || serverQualityProfile.TimeStamp > localQualityProfile.ProfileTimestamp; - } - } -} diff --git a/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs b/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs index 0841d4937f..23477121ce 100644 --- a/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs +++ b/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs @@ -19,10 +19,9 @@ */ using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles { @@ -39,93 +38,17 @@ internal interface IQualityProfileDownloader [Export(typeof(IQualityProfileDownloader))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] - internal class RoslynQualityProfileDownloader( - IBindingConfigProvider bindingConfigProvider, - IConfigurationPersister configurationPersister, - IOutOfDateQualityProfileFinder outOfDateQualityProfileFinder, - ILogger logger, - ILanguageProvider languageProvider) + [ExcludeFromCodeCoverage] // todo https://sonarsource.atlassian.net/browse/SLVS-2420 + internal class RoslynQualityProfileDownloader() : IQualityProfileDownloader { - private readonly IEnumerable languagesToBind = languageProvider.RoslynLanguages; - public async Task UpdateAsync( BoundServerProject boundProject, IProgress progress, CancellationToken cancellationToken) { - var isChanged = false; - - LogWithBindingPrefix(QualityProfilesStrings.UpdatingQualityProfiles); - - EnsureProfilesExistForAllSupportedLanguages(boundProject); - - var outOfDateProfiles = await outOfDateQualityProfileFinder.GetAsync(boundProject, cancellationToken); - - int currentLanguage = 0; - var totalLanguages = outOfDateProfiles.Count; - - LogWithBindingPrefix(string.Format(QualityProfilesStrings.OutOfDateQPsFound, totalLanguages)); - - foreach (var (language, qualityProfileInfo) in outOfDateProfiles) - { - if (!languagesToBind.Contains(language)) - { - continue; - } - currentLanguage++; - - var progressMessage = string.Format(QualityProfilesStrings.DownloadingQualityProfileProgressMessage, language.Name); - progress?.Report(new FixedStepsProgress(progressMessage, currentLanguage, totalLanguages)); - - UpdateProfile(boundProject, language, qualityProfileInfo); - - // (1) Save the top-level binding.config file that contains the QP timestamp - var bindingConfiguration = configurationPersister.Persist(boundProject); - - // (2) Save the language-specific rules config. - // Note: we've already saved the updated timestamp for the current language in the binding.config file at step (1) above - // If there is an error between (1) and (2) then the timestamp in the binding.config will show this language - // is up to date when it is not. - await bindingConfigProvider.SaveConfigurationAsync(qualityProfileInfo, language, bindingConfiguration, cancellationToken); - isChanged = true; - - LogWithBindingPrefix(string.Format(BindingStrings.SubTextPaddingFormat, - string.Format(QualityProfilesStrings.QualityProfileDownloadSuccessfulMessageFormat, qualityProfileInfo.Name, qualityProfileInfo.Key, language.Name))); - } - - if (!isChanged) - { - LogWithBindingPrefix(string.Format(QualityProfilesStrings.SubTextPaddingFormat, QualityProfilesStrings.DownloadingQualityProfilesNotNeeded)); - } - - return isChanged; - } - - /// - /// Ensures that the bound project has a profile entry for every supported language - /// - /// If we add support for new language in the future, this method will make sure it's - /// Quality Profile is fetched next time an update is triggered - private void EnsureProfilesExistForAllSupportedLanguages(BoundServerProject boundProject) - { - if (boundProject.Profiles == null) - { - boundProject.Profiles = new Dictionary(); - } - - foreach (var language in languagesToBind) - { - if (!boundProject.Profiles.ContainsKey(language)) - { - boundProject.Profiles[language] = new ApplicableQualityProfile { ProfileKey = null, ProfileTimestamp = DateTime.MinValue, }; - } - } + // TODO by https://sonarsource.atlassian.net/browse/SLVS-2420 drop this class + return false; } - - private static void UpdateProfile(BoundServerProject boundSonarQubeProject, Language language, SonarQubeQualityProfile serverProfile) => - boundSonarQubeProject.Profiles[language] = new ApplicableQualityProfile { ProfileKey = serverProfile.Key, ProfileTimestamp = serverProfile.TimeStamp }; - - private void LogWithBindingPrefix(string text) => logger.WriteLine(QualityProfilesStrings.QPMessagePrefix + text); } } diff --git a/src/ConnectedMode/ServerIssueFinder.cs b/src/ConnectedMode/ServerIssueFinder.cs deleted file mode 100644 index 6e44515185..0000000000 --- a/src/ConnectedMode/ServerIssueFinder.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarQube.Client; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - public interface IServerIssueFinder - { - Task FindServerIssueAsync(IFilterableIssue localIssue, CancellationToken token); - } - - [Export(typeof(IServerIssueFinder))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class ServerIssueFinder : IServerIssueFinder - { - private readonly IProjectRootCalculator projectRootCalculator; - private readonly IIssueMatcher issueMatcher; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IStatefulServerBranchProvider serverBranchProvider; - private readonly ISonarQubeService sonarQubeService; - private readonly IThreadHandling threadHandling; - - [ImportingConstructor] - public ServerIssueFinder(IProjectRootCalculator projectRootCalculator, IIssueMatcher issueMatcher, - IActiveSolutionBoundTracker activeSolutionBoundTracker, IStatefulServerBranchProvider serverBranchProvider, - ISonarQubeService sonarQubeService, IThreadHandling threadHandling) - { - this.projectRootCalculator = projectRootCalculator; - this.issueMatcher = issueMatcher; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.serverBranchProvider = serverBranchProvider; - this.sonarQubeService = sonarQubeService; - this.threadHandling = threadHandling; - } - - public async Task FindServerIssueAsync(IFilterableIssue localIssue, CancellationToken token) - { - threadHandling.ThrowIfOnUIThread(); - - var bindingConfiguration = activeSolutionBoundTracker.CurrentConfiguration; - - if (bindingConfiguration.Mode == SonarLintMode.Standalone) - { - return null; - } - - var projectRoot = await projectRootCalculator.CalculateBasedOnLocalPathAsync(localIssue.FilePath, token); - - if (projectRoot == null) - { - return null; - } - - var componentKey = ComponentKeyGenerator.GetComponentKey(localIssue.FilePath, - projectRoot, - bindingConfiguration.Project.ServerProjectKey); - - var serverIssues = await sonarQubeService.GetIssuesForComponentAsync( - bindingConfiguration.Project.ServerProjectKey, - await serverBranchProvider.GetServerBranchNameAsync(token), - componentKey, - localIssue.RuleId, - token); - - return issueMatcher.GetFirstLikelyMatchFromSameFileOrNull(localIssue, serverIssues); - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs b/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs deleted file mode 100644 index 48d2a2dc57..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventsListener.cs +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - /// Handles coming from the server. - /// - internal interface IIssueServerEventsListener : IDisposable - { - Task ListenAsync(); - } - - [Export(typeof(IIssueServerEventsListener))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class IssueServerEventsListener : IIssueServerEventsListener - { - private readonly IIssueServerEventSource issueServerEventSource; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IThreadHandling threadHandling; - private readonly IStatefulServerBranchProvider branchProvider; - private readonly ILogger logger; - private readonly CancellationTokenSource cancellationTokenSource; - - [ImportingConstructor] - public IssueServerEventsListener( - IIssueServerEventSource issueServerEventSource, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IStatefulServerBranchProvider branchProvider, - IThreadHandling threadHandling, - ILogger logger) - { - this.issueServerEventSource = issueServerEventSource; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.branchProvider = branchProvider; - this.threadHandling = threadHandling; - this.logger = logger; - - cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task ListenAsync() - { - await threadHandling.SwitchToBackgroundThread(); - - while (!cancellationTokenSource.IsCancellationRequested) - { - try - { - var issueServerEvent = await issueServerEventSource.GetNextEventOrNullAsync(); - - if (issueServerEvent == null) - { - // Will return null when issueServerEventSource is disposed - return; - } - - var serverBranch = await branchProvider.GetServerBranchNameAsync(cancellationTokenSource.Token); - - var issueKeysForCurrentBranch = issueServerEvent.BranchAndIssueKeys - .Where(x => x.BranchName.Equals(serverBranch)) - .Select(x => x.IssueKey) - .ToArray(); - - logger.LogVerbose(Resources.Suppression_IssueChangedEvent, issueServerEvent.IsResolved, string.Join(",", issueKeysForCurrentBranch)); - - if (issueKeysForCurrentBranch.Any()) - { - await roslynSuppressionUpdater.UpdateSuppressedIssuesAsync(issueServerEvent.IsResolved, issueKeysForCurrentBranch, cancellationTokenSource.Token); - } - - logger.LogVerbose(Resources.Suppression_IssueChangedEventFinished); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose($"[IssueServerEventsListener] Failed to handle issue event: {ex}"); - } - } - } - - private bool disposed; - - public void Dispose() - { - if (disposed) - { - return; - } - - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - disposed = true; - } - } -} diff --git a/src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs b/src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs deleted file mode 100644 index 37e5c99f75..0000000000 --- a/src/ConnectedMode/Suppressions/IRoslynSuppressionUpdater.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Suppressions; - -/// -/// Fetches suppressed issues from the server and raises events. This is mainly needed for Roslyn languages -/// -public interface IRoslynSuppressionUpdater -{ - /// - /// Fetches all available suppressions from the server and raises the event. - /// - Task UpdateAllServerSuppressionsAsync(); - - /// - /// For resolved issues, the event is invoked. - /// For unresolved issues (i.e. issues that are re-opened), the event is invoked. - /// - Task UpdateSuppressedIssuesAsync(bool isResolved, string[] issueKeys, CancellationToken cancellationToken); - - event EventHandler SuppressedIssuesReloaded; - event EventHandler NewIssuesSuppressed; - event EventHandler SuppressionsRemoved; -} - -public class SuppressionsEventArgs(IReadOnlyList suppressedIssues) : EventArgs -{ - public IReadOnlyList SuppressedIssues { get; } = suppressedIssues; -} - -public class SuppressionsRemovedEventArgs(IReadOnlyList issueServerKeys) : EventArgs -{ - public IReadOnlyList IssueServerKeys { get; } = issueServerKeys; -} diff --git a/src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs b/src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs deleted file mode 100644 index e1fab5e7dc..0000000000 --- a/src/ConnectedMode/Suppressions/RoslynSuppressionUpdater.cs +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.Core; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Suppressions; - -[Export(typeof(IRoslynSuppressionUpdater))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class RoslynSuppressionUpdater : IRoslynSuppressionUpdater, IDisposable -{ - private readonly ICancellableActionRunner actionRunner; - private readonly ILogger logger; - private readonly ISonarQubeService server; - private readonly IServerQueryInfoProvider serverQueryInfoProvider; - private readonly IThreadHandling threadHandling; - - [ImportingConstructor] - public RoslynSuppressionUpdater( - ISonarQubeService server, - IServerQueryInfoProvider serverQueryInfoProvider, - ICancellableActionRunner actionRunner, - ILogger logger, - IThreadHandling threadHandling) - { - this.server = server; - this.serverQueryInfoProvider = serverQueryInfoProvider; - this.actionRunner = actionRunner; - this.logger = logger.ForContext(Resources.ConnectedModeLogContext, Resources.RoslynSuppressionsLogContext); - this.threadHandling = threadHandling; - } - - public async Task UpdateAllServerSuppressionsAsync() - { - var suppressedIssues = await GetSuppressedIssuesAsync(); - if (suppressedIssues?.Count > 0) - { - InvokeSuppressedIssuesReloaded(suppressedIssues); - } - } - - public async Task UpdateSuppressedIssuesAsync(bool isResolved, string[] issueKeys, CancellationToken cancellationToken) - { - if (!issueKeys.Any()) - { - return; - } - if (!isResolved) - { - InvokeSuppressionsRemoved(issueKeys); - return; - } - - var suppressedIssues = await GetSuppressedIssuesAsync(issueKeys, cancellationToken); - if (suppressedIssues?.Count > 0) - { - InvokeNewIssuesSuppressed(suppressedIssues); - } - } - - public event EventHandler SuppressedIssuesReloaded; - public event EventHandler NewIssuesSuppressed; - public event EventHandler SuppressionsRemoved; - - public void Dispose() => actionRunner.Dispose(); - - private async Task> GetSuppressedIssuesAsync(string[] issueKeys = null, CancellationToken? cancellationToken = null) => - await threadHandling.RunOnBackgroundThread(async () => - { - IList suppressedIssues = null; - await actionRunner.RunAsync(async token => - { - suppressedIssues = await FetchSuppressedSonarQubeIssuesAsync(issueKeys, cancellationToken, token); - }); - - return suppressedIssues; - }); - - private async Task> FetchSuppressedSonarQubeIssuesAsync( - string[] issueKeys, - CancellationToken? cancellationToken, - CancellationToken token) - { - IList suppressedIssues = null; - try - { - var allServerIssuesFetched = issueKeys == null; - logger.WriteLine(Resources.Suppressions_Fetch_Issues, allServerIssuesFetched); - - var (projectKey, serverBranch) = await serverQueryInfoProvider.GetProjectKeyAndBranchAsync(token); - if (projectKey == null || serverBranch == null) - { - return null; - } - - ThrowIfCancelled(cancellationToken, token); - suppressedIssues = await server.GetSuppressedRoslynIssuesAsync(projectKey, serverBranch, issueKeys, token); - logger.WriteLine(Resources.Suppression_Fetch_Issues_Finished, allServerIssuesFetched); - } - catch (OperationCanceledException) - { - logger.WriteLine(Resources.Suppressions_FetchOperationCancelled); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose(Resources.Suppression_FetchError_Verbose, ex); - logger.WriteLine(Resources.Suppressions_FetchError_Short, ex.Message); - } - return suppressedIssues; - } - - private static void ThrowIfCancelled(CancellationToken? cancellationToken, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - cancellationToken?.ThrowIfCancellationRequested(); - } - - private void InvokeSuppressedIssuesReloaded(IList allSuppressedIssues) => SuppressedIssuesReloaded?.Invoke(this, new SuppressionsEventArgs(allSuppressedIssues.ToList())); - - private void InvokeSuppressionsRemoved(IList suppressedIssueKeys) => SuppressionsRemoved?.Invoke(this, new SuppressionsRemovedEventArgs(suppressedIssueKeys.ToList())); - - private void InvokeNewIssuesSuppressed(IList newSuppressedIssues) => NewIssuesSuppressed?.Invoke(this, new SuppressionsEventArgs(newSuppressedIssues.ToList())); -} diff --git a/src/ConnectedMode/Suppressions/TimedUpdateHandler.cs b/src/ConnectedMode/Suppressions/TimedUpdateHandler.cs deleted file mode 100644 index 43ccaf93db..0000000000 --- a/src/ConnectedMode/Suppressions/TimedUpdateHandler.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.SystemAbstractions; - -namespace SonarLint.VisualStudio.ConnectedMode.Suppressions -{ - [Export(typeof(TimedUpdateHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class TimedUpdateHandler : IDisposable - { - private const double MillisecondsToWaitBetweenRefresh = 1000 * 60 * 10; // 10 minutes - - private readonly ITimer refreshTimer; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IQualityProfileUpdater qualityProfileUpdater; - private readonly ILogger logger; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - - private bool disposed; - - [ImportingConstructor] - public TimedUpdateHandler( - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IQualityProfileUpdater qualityProfileUpdater, - ILogger logger, - IActiveSolutionBoundTracker activeSolutionBoundTracker) - : this(roslynSuppressionUpdater, qualityProfileUpdater, activeSolutionBoundTracker, logger, new TimerFactory()) - { - } - - internal /* for testing */ TimedUpdateHandler( - IRoslynSuppressionUpdater roslynSuppressionUpdater, - IQualityProfileUpdater qualityProfileUpdater, - IActiveSolutionBoundTracker activeSolutionBoundTracker, - ILogger logger, - ITimerFactory timerFactory) - { - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.qualityProfileUpdater = qualityProfileUpdater; - this.logger = logger; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - - refreshTimer = timerFactory.Create(); - refreshTimer.AutoReset = true; - refreshTimer.Interval = MillisecondsToWaitBetweenRefresh; - refreshTimer.Elapsed += OnRefreshTimerElapsed; - - activeSolutionBoundTracker.SolutionBindingChanged += SolutionBindingChanged; - - SetTimerStatus(activeSolutionBoundTracker.CurrentConfiguration); - } - - private void SolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs activeSolutionBindingEventArgs) - { - SetTimerStatus(activeSolutionBindingEventArgs.Configuration); - } - - private void SetTimerStatus(BindingConfiguration bindingConfiguration) - { - if (bindingConfiguration?.Mode != SonarLintMode.Standalone) - { - refreshTimer.Start(); - } - else - { - refreshTimer.Stop(); - } - } - - private void OnRefreshTimerElapsed(object sender, TimerEventArgs e) - { - logger.WriteLine(Resources.TimedUpdateTriggered); - roslynSuppressionUpdater.UpdateAllServerSuppressionsAsync().Forget(); - qualityProfileUpdater.UpdateAsync().Forget(); - } - - public void Dispose() - { - if (!disposed) - { - refreshTimer.Elapsed -= OnRefreshTimerElapsed; - refreshTimer.Dispose(); - disposed = true; - - activeSolutionBoundTracker.SolutionBindingChanged -= SolutionBindingChanged; - } - } - } -} diff --git a/src/ConnectedMode/Transition/MuteIssuesService.cs b/src/ConnectedMode/Transition/MuteIssuesService.cs index f8dc6d6ccf..f21ac66279 100644 --- a/src/ConnectedMode/Transition/MuteIssuesService.cs +++ b/src/ConnectedMode/Transition/MuteIssuesService.cs @@ -19,7 +19,6 @@ */ using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.Core.Suppressions; @@ -39,8 +38,6 @@ internal class MuteIssuesService( IMuteIssuesWindowService muteIssuesWindowService, IActiveConfigScopeTracker activeConfigScopeTracker, ISLCoreServiceProvider slCoreServiceProvider, - IServerIssueFinder serverIssueFinder, - IRoslynSuppressionUpdater roslynSuppressionUpdater, ILogger logger, IThreadHandling threadHandling) : IMuteIssuesService @@ -50,34 +47,20 @@ internal class MuteIssuesService( public async Task ResolveIssueWithDialogAsync(IFilterableIssue issue) { threadHandling.ThrowIfOnUIThread(); - var issueServerKey = await GetIssueServerKeyAsync(issue); + var issueServerKey = GetIssueServerKey(issue); var currentConfigScope = activeConfigScopeTracker.Current; CheckIsInConnectedMode(currentConfigScope); CheckIssueServerKeyNotNullOrEmpty(issueServerKey); var allowedStatuses = await GetAllowedStatusesAsync(currentConfigScope.ConnectionId, issueServerKey); var windowResponse = await PromptMuteIssueResolutionAsync(allowedStatuses); - await MuteIssueAsync(currentConfigScope.Id, issueServerKey, issue, windowResponse.IssueTransition.Value); + await MuteIssueAsync(currentConfigScope.Id, issueServerKey, windowResponse.IssueTransition.Value); await AddCommentAsync(currentConfigScope.Id, issueServerKey, windowResponse.Comment); } - private async Task GetIssueServerKeyAsync(IFilterableIssue issue) - { - // Non-Roslyn issues already have the issue server key - if (issue is IAnalysisIssueVisualization issueViz) - { - return issueViz.Issue.IssueServerKey; - } - - // Roslyn issues need to be converted to SonarQube issues to get the server key as they are handled by SLCore - var serverIssue = await serverIssueFinder.FindServerIssueAsync(issue, CancellationToken.None); - if (serverIssue is { IsResolved: true }) - { - logger.WriteLine(Resources.MuteIssue_ErrorIssueAlreadyResolved); - throw new MuteIssueException(Resources.MuteIssue_ErrorIssueAlreadyResolved); - } - return serverIssue?.IssueKey; - } + private static string GetIssueServerKey(IFilterableIssue issue) => + // TODO by https://sonarsource.atlassian.net/browse/SLVS-2419 remove handling of different type of issues + ((IAnalysisIssueVisualization)issue).Issue.IssueServerKey; private async Task PromptMuteIssueResolutionAsync(IEnumerable allowedStatuses) { @@ -153,7 +136,6 @@ private async Task> GetAllowedStatusesAsync(string connec private async Task MuteIssueAsync( string configurationScopeId, string issueServerKey, - IFilterableIssue issue, SonarQubeIssueTransition transition) { try @@ -166,7 +148,6 @@ await issueSlCoreService.ChangeStatusAsync(new ChangeIssueStatusParams transition.ToSlCoreResolutionStatus(), false // Muting taints are not supported yet )); - await UpdateRoslynSuppressionsAsync(issue, issueServerKey); } catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { @@ -191,15 +172,4 @@ private async Task AddCommentAsync(string configurationScopeId, string issueServ throw new MuteIssueException.MuteIssueCommentFailedException(); } } - - /// - /// The suppressed issues for roslyn are not dealt by SlCore, but are stored on disk, so we need to update them manually - /// - private async Task UpdateRoslynSuppressionsAsync(IFilterableIssue issue, string serverIssueKey) - { - if (issue is IFilterableRoslynIssue) - { - await roslynSuppressionUpdater.UpdateSuppressedIssuesAsync(isResolved: true, [serverIssueKey], new CancellationToken()); - } - } } diff --git a/src/Core/CSharpVB/IRoslynConfigGenerator.cs b/src/Core/CSharpVB/IRoslynConfigGenerator.cs deleted file mode 100644 index f357d4ebd4..0000000000 --- a/src/Core/CSharpVB/IRoslynConfigGenerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Core.CSharpVB; - -public interface IRoslynConfigGenerator -{ - void GenerateAndSaveConfiguration( - RoslynLanguage language, - string baseDirectory, - IDictionary properties, - IFileExclusions fileExclusions, - IReadOnlyCollection ruleStatuses, - IReadOnlyCollection ruleParameters); -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs deleted file mode 100644 index d1004fa508..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/SolutionRoslynAnalyzerManagerTests.cs +++ /dev/null @@ -1,381 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using NSubstitute.ClearExtensions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class SolutionRoslynAnalyzerManagerTests -{ - private static readonly IEqualityComparer DefaultComparer = EqualityComparer.Default; - private IBasicRoslynAnalyzerProvider basicRoslynAnalyzerProvider; - private IEnterpriseRoslynAnalyzerProvider enterpriseRoslynAnalyzerProvider; - private IRoslynWorkspaceWrapper roslynWorkspaceWrapper; - private IActiveConfigScopeTracker activeConfigScopeTracker; - private IActiveSolutionTracker activeSolutionTracker; - private IAsyncLockFactory asyncLockFactory; - private IAsyncLock asyncLock; - private IEqualityComparer?> analyzerComparer; - private TestLogger logger; - private SolutionRoslynAnalyzerManager testSubject; - private readonly ImmutableArray embeddedAnalyzers = ImmutableArray.Create(new AnalyzerFileReference(@"C:\path\embedded", Substitute.For())); - private readonly ImmutableArray connectedAnalyzers = - ImmutableArray.Create( - new AnalyzerFileReference(@"C:\path\connected1", Substitute.For()), - new AnalyzerFileReference(@"C:\path\connected2", Substitute.For())); - - [TestInitialize] - public void TestInitialize() - { - logger = new TestLogger(); - basicRoslynAnalyzerProvider = Substitute.For(); - enterpriseRoslynAnalyzerProvider = Substitute.For(); - roslynWorkspaceWrapper = Substitute.For(); - analyzerComparer = Substitute.For?>>(); - activeConfigScopeTracker = Substitute.For(); - activeSolutionTracker = Substitute.For(); - asyncLockFactory = Substitute.For(); - asyncLock = Substitute.For(); - asyncLockFactory.Create().Returns(asyncLock); - - testSubject = new SolutionRoslynAnalyzerManager( - basicRoslynAnalyzerProvider, - enterpriseRoslynAnalyzerProvider, - roslynWorkspaceWrapper, - analyzerComparer, - activeConfigScopeTracker, - activeSolutionTracker, - asyncLockFactory, - logger); - } - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_SubscribesToEvents() - { - new SolutionRoslynAnalyzerManager( - basicRoslynAnalyzerProvider, - enterpriseRoslynAnalyzerProvider, - roslynWorkspaceWrapper, - analyzerComparer, - activeConfigScopeTracker, - activeSolutionTracker, - asyncLockFactory, - logger); - - // todo add more tests for the events - activeConfigScopeTracker.Received().CurrentConfigurationScopeChanged += Arg.Any>(); - activeSolutionTracker.Received().ActiveSolutionChanged += Arg.Any>(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_NoOpenSolution_DoesNothing() - { - await testSubject.OnSolutionStateChangedAsync(null); - - roslynWorkspaceWrapper.ReceivedCalls().Should().BeEmpty(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_AcquiresLock() - { - await testSubject.OnSolutionStateChangedAsync(null); - - await asyncLock.Received(1).AcquireAsync(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_AppliesEmbeddedAnalyzer() - { - basicRoslynAnalyzerProvider.GetBasicAsync() - .Returns(embeddedAnalyzers); - var solution = Substitute.For(); - SetUpAnalyzerUpdate([], embeddedAnalyzers, solution); - - await testSubject.OnSolutionStateChangedAsync("solution"); - - roslynWorkspaceWrapper.Received().TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - AssertNoErrorsInLogs(); - } - - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_UpdateFails_Logs() - { - basicRoslynAnalyzerProvider.GetBasicAsync() - .Returns(embeddedAnalyzers); - IRoslynSolutionWrapper failedUpdate = null; - SetUpAnalyzerUpdate([], embeddedAnalyzers, failedUpdate); - - await testSubject.OnSolutionStateChangedAsync("solution"); - - roslynWorkspaceWrapper.Received().TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - AssertUpdateFailedAndLogged(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_BindingSet_RemovesStandaloneAndAppliesConnectedAnalyzer() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - analyzerComparer.Equals(embeddedAnalyzers, connectedAnalyzers).Returns(false); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns(connectedAnalyzers); - SetUpAnalyzerUpdate(embeddedAnalyzers, connectedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(solutionName); - - analyzerComparer.Received().Equals(embeddedAnalyzers, connectedAnalyzers); - roslynWorkspaceWrapper.Received().TryApplyChangesAsync(Arg.Is(x => - x.AnalyzersToAdd.SequenceEqual(connectedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer))); - basicRoslynAnalyzerProvider.DidNotReceiveWithAnyArgs().GetBasicAsync(); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_StandaloneSolution_BindingSet_NoConnectedAnalyzer_DoesNotReRegisterEmbedded() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - EnableDefaultEmbeddedAnalyzers(); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns((ImmutableArray?)null); - analyzerComparer.Equals(embeddedAnalyzers, embeddedAnalyzers).Returns(true); - - await testSubject.OnSolutionStateChangedAsync(solutionName); - - Received.InOrder(() => - { - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName); - basicRoslynAnalyzerProvider.GetBasicAsync(); - analyzerComparer.Equals(embeddedAnalyzers, embeddedAnalyzers); - }); - roslynWorkspaceWrapper.DidNotReceiveWithAnyArgs().TryApplyChangesAsync(default); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosedAndReopened_RegistersAnalyzersAgain() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - EnableDefaultEmbeddedAnalyzers(); - SetUpAnalyzerUpdate(embeddedAnalyzers, [], Substitute.For()); - SetUpAnalyzerUpdate([], embeddedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(null); - await testSubject.OnSolutionStateChangedAsync(solutionName); - - Received.InOrder(() => - { - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.Length == 0 && x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer))); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName); - basicRoslynAnalyzerProvider.GetBasicAsync(); - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - }); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosedAndReopenedAsBound_RegistersConnectedAnalyzers() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns(connectedAnalyzers); - - SetUpAnalyzerUpdate(embeddedAnalyzers, [], Substitute.For()); - SetUpAnalyzerUpdate([], connectedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(null); - await testSubject.OnSolutionStateChangedAsync(solutionName); - - Received.InOrder(() => - { - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.Length == 0 && x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer))); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName); - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => x.AnalyzersToAdd.SequenceEqual(connectedAnalyzers, DefaultComparer) && x.AnalyzersToRemove.Length == 0)); - }); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_DifferentSolutionOpened_RegistersAnalyzers() - { - await SetUpStandaloneSolution("original solution"); - var differentSolution = "different solution"; - EnableDefaultEmbeddedAnalyzers(); - - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(differentSolution).Returns(connectedAnalyzers); - SetUpAnalyzerUpdate(embeddedAnalyzers, connectedAnalyzers, Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(differentSolution); - - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Is(x => - x.AnalyzersToRemove.SequenceEqual(embeddedAnalyzers, DefaultComparer) && x.AnalyzersToAdd.SequenceEqual(connectedAnalyzers, DefaultComparer))); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public void Dispose_UnsubscribesFromEvents() - { - testSubject.Dispose(); - - activeConfigScopeTracker.Received().CurrentConfigurationScopeChanged -= Arg.Any>(); - activeSolutionTracker.Received().ActiveSolutionChanged -= Arg.Any>(); - } - - [TestMethod] - public void OnSolutionStateChangedAsync_Disposed_Throws() - { - var act = async () => await testSubject.OnSolutionStateChangedAsync("solution"); - testSubject.Dispose(); - - act.Should().Throw(); - } - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosed_RemovesAnalzyers() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - SetUpAnalyzerUpdate(embeddedAnalyzers, [], Substitute.For()); - - await testSubject.OnSolutionStateChangedAsync(null); - - await enterpriseRoslynAnalyzerProvider.DidNotReceiveWithAnyArgs().GetEnterpriseOrNullAsync(solutionName); - AssertNoErrorsInLogs(); - } - - - [TestMethod] - public async Task OnSolutionStateChangedAsync_SolutionClosed_FailedToRemove_Logs() - { - const string solutionName = "solution"; - await SetUpStandaloneSolution(solutionName); - SetUpAnalyzerUpdate(embeddedAnalyzers, [], null); - - await testSubject.OnSolutionStateChangedAsync(null); - - await enterpriseRoslynAnalyzerProvider.DidNotReceiveWithAnyArgs().GetEnterpriseOrNullAsync(solutionName); - AssertRemoveFailedAndLogged(); - } - - [TestMethod] - public async Task CurrentConfigurationScopeChanged_SetsAnalyzers() - { - const string mySolution = "my solution"; - roslynWorkspaceWrapper.TryApplyChangesAsync(default).ReturnsForAnyArgs(Substitute.For()); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(mySolution).Returns(embeddedAnalyzers); - activeConfigScopeTracker.Current.Returns(new ConfigurationScope(mySolution)); - - activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith(new(default)); - - await enterpriseRoslynAnalyzerProvider.Received(1).GetEnterpriseOrNullAsync(mySolution); - roslynWorkspaceWrapper.ReceivedWithAnyArgs(1).TryApplyChangesAsync(default); - AssertNoErrorsInLogs(); - } - - [TestMethod] - public async Task ActiveSolutionChanged_SetsAnalyzers() - { - const string solutionName = "my solution"; - roslynWorkspaceWrapper.TryApplyChangesAsync(default).ReturnsForAnyArgs(Substitute.For()); - enterpriseRoslynAnalyzerProvider.GetEnterpriseOrNullAsync(solutionName).Returns(embeddedAnalyzers); - - activeSolutionTracker.ActiveSolutionChanged += Raise.Event>(this, new ActiveSolutionChangedEventArgs(true, solutionName)); - - await enterpriseRoslynAnalyzerProvider.Received(1).GetEnterpriseOrNullAsync(solutionName); - roslynWorkspaceWrapper.ReceivedWithAnyArgs(1).TryApplyChangesAsync(default); - AssertNoErrorsInLogs(); - } - - private void SetUpAnalyzerUpdate( - IReadOnlyList analyzersToRemove, - IReadOnlyList analyzersToAdd, - IRoslynSolutionWrapper resultingSolution) - { - analyzersToRemove ??= []; - analyzersToAdd ??= []; - roslynWorkspaceWrapper.TryApplyChangesAsync( - Arg.Is( - x => - x.AnalyzersToRemove.SequenceEqual(analyzersToRemove, DefaultComparer) - && x.AnalyzersToAdd.SequenceEqual(analyzersToAdd, DefaultComparer))) - .Returns(resultingSolution); - } - - private void AssertNoErrorsInLogs() - { - logger.AssertPartialOutputStringDoesNotExist(Resources.RoslynAnalyzersNotUpdated); - logger.AssertPartialOutputStringDoesNotExist(Resources.RoslynAnalyzersNotRemoved); - } - private void AssertUpdateFailedAndLogged() => logger.AssertPartialOutputStringExists(Resources.RoslynAnalyzersNotUpdated); - private void AssertRemoveFailedAndLogged() => logger.AssertPartialOutputStringExists(Resources.RoslynAnalyzersNotRemoved); - - private void EnableDefaultEmbeddedAnalyzers() - { - basicRoslynAnalyzerProvider.GetBasicAsync().Returns(embeddedAnalyzers); - } - - private async Task SetUpStandaloneSolution(string solutionName) - { - EnableDefaultEmbeddedAnalyzers(); - await SimulateSolutionSet(Substitute.For(), solutionName); - } - - private async Task SimulateSolutionSet(IRoslynSolutionWrapper resultingSolution, string solutionName) - { - roslynWorkspaceWrapper.TryApplyChangesAsync(Arg.Any()).Returns(resultingSolution); - await testSubject.OnSolutionStateChangedAsync(solutionName); - ClearSubstitutes(); - } - - private void ClearSubstitutes() - { - basicRoslynAnalyzerProvider.ClearSubstitute(); - enterpriseRoslynAnalyzerProvider.ClearSubstitute(); - analyzerComparer.ClearSubstitute(); - roslynWorkspaceWrapper.ClearSubstitute(); - } -} diff --git a/src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs b/src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs deleted file mode 100644 index 61d353e824..0000000000 --- a/src/Infrastructure.VS/Roslyn/SolutionRoslynAnalyzerManager.cs +++ /dev/null @@ -1,193 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Text; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -public interface ISolutionRoslynAnalyzerManager : IDisposable -{ -} - -[Export(typeof(ISolutionRoslynAnalyzerManager))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class SolutionRoslynAnalyzerManager : ISolutionRoslynAnalyzerManager -{ - private static readonly ImmutableArray NoChange = ImmutableArray.Empty; - private bool disposed; - private readonly IEqualityComparer?> analyzerComparer; - private readonly IActiveConfigScopeTracker activeConfigScopeTracker; - private readonly IActiveSolutionTracker activeSolutionTracker; - private readonly IAsyncLock asyncLock; - private readonly IEnterpriseRoslynAnalyzerProvider enterpriseAnalyzerProvider; - private readonly IBasicRoslynAnalyzerProvider basicAnalyzerProvider; - private readonly ILogger logger; - private readonly IRoslynWorkspaceWrapper roslynWorkspace; - private ImmutableArray? currentAnalyzers; - - [ImportingConstructor] - public SolutionRoslynAnalyzerManager( - IBasicRoslynAnalyzerProvider basicAnalyzerProvider, - IEnterpriseRoslynAnalyzerProvider enterpriseAnalyzerProvider, - IRoslynWorkspaceWrapper roslynWorkspace, - IActiveConfigScopeTracker activeConfigScopeTracker, - IActiveSolutionTracker activeSolutionTracker, - IAsyncLockFactory asyncLockFactory, - ILogger logger) - : this(basicAnalyzerProvider, - enterpriseAnalyzerProvider, - roslynWorkspace, - AnalyzerArrayComparer.Instance, - activeConfigScopeTracker, - activeSolutionTracker, - asyncLockFactory, - logger) - { - } - - internal /* for testing */ SolutionRoslynAnalyzerManager( - IBasicRoslynAnalyzerProvider basicAnalyzerProvider, - IEnterpriseRoslynAnalyzerProvider enterpriseAnalyzerProvider, - IRoslynWorkspaceWrapper roslynWorkspace, - IEqualityComparer?> analyzerComparer, - IActiveConfigScopeTracker activeConfigScopeTracker, - IActiveSolutionTracker activeSolutionTracker, - IAsyncLockFactory asyncLockFactory, - ILogger logger) - { - this.basicAnalyzerProvider = basicAnalyzerProvider; - this.enterpriseAnalyzerProvider = enterpriseAnalyzerProvider; - this.roslynWorkspace = roslynWorkspace; - this.analyzerComparer = analyzerComparer; - this.activeConfigScopeTracker = activeConfigScopeTracker; - this.activeSolutionTracker = activeSolutionTracker; - this.asyncLock = asyncLockFactory.Create(); - this.logger = logger.ForVerboseContext("Roslyn Analyzers"); - - activeConfigScopeTracker.CurrentConfigurationScopeChanged += OnConfigurationScopeChanged; - activeSolutionTracker.ActiveSolutionChanged += OnActiveSolutionChanged; - } - - internal /*for testing*/ async Task OnSolutionStateChangedAsync(string solutionName) - { - using (await asyncLock.AcquireAsync()) - { - ThrowIfDisposed(); - - if (solutionName is null) - { - await RemoveCurrentAnalyzersAsync(); - return; - } - try - { - await UpdateAnalyzersIfChangedAsync(await ChooseAnalyzersAsync(solutionName)); - } - catch (Exception e) - { - logger.WriteLine(e.ToString()); - } - } - } - - private void ThrowIfDisposed() - { - if (disposed) - { - throw new ObjectDisposedException(nameof(SolutionRoslynAnalyzerManager)); - } - } - - public void Dispose() - { - if (disposed) - { - return; - } - activeConfigScopeTracker.CurrentConfigurationScopeChanged -= OnConfigurationScopeChanged; - activeSolutionTracker.ActiveSolutionChanged -= OnActiveSolutionChanged; - disposed = true; - } - - private async Task> ChooseAnalyzersAsync(string configurationScopeId) => - await enterpriseAnalyzerProvider.GetEnterpriseOrNullAsync(configurationScopeId) ?? await basicAnalyzerProvider.GetBasicAsync(); - - private async Task UpdateAnalyzersIfChangedAsync(ImmutableArray analyzersToUse) - { - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["To Update"] }, PrintAnalyzersChoice(analyzersToUse)); - if (!DidAnalyzerChoiceChange(analyzersToUse)) - { - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["No Update"] }, "Nothing to update"); - return; - } - - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["Before Update"] }, roslynWorkspace.CurrentSolution?.DisplayCurrentAnalyzerState()); - var updatedSolution = await UpdateAnalyzersAsync(analyzersToUse); - logger.LogVerbose(new MessageLevelContext { VerboseContext = ["After Update"] }, updatedSolution?.DisplayCurrentAnalyzerState()); - } - - private bool DidAnalyzerChoiceChange(ImmutableArray analyzersToUse) => !analyzerComparer.Equals(currentAnalyzers, analyzersToUse); - - private async Task RemoveCurrentAnalyzersAsync() - { - if (!currentAnalyzers.HasValue || await roslynWorkspace.TryApplyChangesAsync(new AnalyzerChange(currentAnalyzers.Value, NoChange)) is not null) - { - currentAnalyzers = null; - return; - } - - logger.LogVerbose(Resources.RoslynAnalyzersNotRemoved); - } - - private async Task UpdateAnalyzersAsync(ImmutableArray analyzersToUse) - { - if (await roslynWorkspace.TryApplyChangesAsync(new AnalyzerChange(currentAnalyzers ?? NoChange, analyzersToUse)) is { } solution) - { - currentAnalyzers = analyzersToUse; - return solution; - } - - logger.WriteLine(Resources.RoslynAnalyzersNotUpdated); - return null; - } - - private static string PrintAnalyzersChoice(ImmutableArray analyzersToUse) - { - var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine("Analyzer update. Registering the following analyzers:"); - foreach (var analyzer in analyzersToUse) - { - stringBuilder.AppendLine($" {analyzer.DisplayInfo()}"); - } - var messageFormat = stringBuilder.ToString(); - return messageFormat; - } - - private void OnConfigurationScopeChanged(object sender, EventArgs e) => OnSolutionStateChangedAsync(activeConfigScopeTracker.Current?.Id).Forget(); - - private void OnActiveSolutionChanged(object sender, ActiveSolutionChangedEventArgs e) => OnSolutionStateChangedAsync(e?.SolutionName).Forget(); -} diff --git a/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs b/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs deleted file mode 100644 index e2e399fcb2..0000000000 --- a/src/Integration.UnitTests/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdaterTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Core.UserRuleSettings; -using SonarLint.VisualStudio.Integration.CSharpVB.StandaloneMode; -using SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB.StandaloneMode; - -[TestClass] -public class StandaloneRoslynSettingsUpdaterTests -{ - private IRoslynConfigGenerator roslynConfigGenerator; - private StandaloneRoslynSettingsUpdater testSubject; - private ILanguageProvider languageProvider; - private IThreadHandling threadHandling; - - [TestInitialize] - public void TestInitialize() - { - roslynConfigGenerator = Substitute.For(); - languageProvider = Substitute.For(); - threadHandling = Substitute.For(); - threadHandling.RunOnBackgroundThread(Arg.Any>>()) - .Returns(info => info.Arg>>()()); - - testSubject = new StandaloneRoslynSettingsUpdater( - roslynConfigGenerator, - languageProvider, - threadHandling); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Update_CallsGeneratorWithCorrectLanguageAndDirectory() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp, FakeRoslynLanguage.Instance]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var userSettings = new UserSettings(new AnalysisSettings(), @"APPDATA\SonarLint for Visual Studio\.global"); - - testSubject.Update(userSettings); - - Received.InOrder(() => - { - foreach (var language in fakeRoslynLanguages) - { - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - @"APPDATA\SonarLint for Visual Studio\.global", - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Is>(x => x.Count == 0)); - } - }); - } - - [TestMethod] - public void Update_CallsGeneratorWithCorrectProperties() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var properties = ImmutableDictionary.Create().SetItem("key", "value"); - var userSettings = new UserSettings(new AnalysisSettings(analysisProperties: properties), "any"); - - testSubject.Update(userSettings); - - Received.InOrder(() => - { - foreach (RoslynLanguage language in fakeRoslynLanguages) - { - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - Arg.Any(), - properties, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Is>(x => x.Count == 0)); - } - }); - } - - [TestMethod] - public void Update_ConvertsExclusionsCorrectly() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, FakeRoslynLanguage.Instance]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - - testSubject.Update(new UserSettings(new AnalysisSettings([], ["one", "two"], []), "any")); - - Received.InOrder(() => - { - foreach (var language in fakeRoslynLanguages) - { - roslynConfigGenerator.GenerateAndSaveConfiguration( - language, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Is(x => x.ToDictionary()["sonar.exclusions"] == "**/one,**/two"), - Arg.Is>(x => x.Count == 0), - Arg.Is>(x => x.Count == 0)); - } - }); - } - - [TestMethod] - public void Update_ConvertsRulesCorrectly() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var rules = new Dictionary() - { - { "vbnet:S1", new RuleConfig(RuleLevel.On, new Dictionary { { "1", "11" } }) }, - { "vbnet:S2", new RuleConfig(RuleLevel.Off, []) }, - { "vbnet:S3", new RuleConfig(RuleLevel.On, []) }, - { "vbnet:S4", new RuleConfig(RuleLevel.Off, new Dictionary { { "4", "44" } }) }, - }; - - testSubject.Update(new UserSettings(new AnalysisSettings(rules, [], []), "any")); - - roslynConfigGenerator - .Received() - .GenerateAndSaveConfiguration( - Language.VBNET, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 4), - Arg.Is>(x => x.Count == 4)); - var statuses = roslynConfigGenerator.ReceivedCalls().Single().GetArguments()[4] as List; - statuses.Should().BeEquivalentTo(new List - { - new(new SonarCompositeRuleId("vbnet", "S1"), true), - new(new SonarCompositeRuleId("vbnet", "S2"), false), - new(new SonarCompositeRuleId("vbnet", "S3"), true), - new(new SonarCompositeRuleId("vbnet", "S4"), false), - }); - var parameters = roslynConfigGenerator.ReceivedCalls().Single().GetArguments()[5] as List; - parameters.Should().BeEquivalentTo(new List - { - new(new SonarCompositeRuleId("vbnet", "S1"), new Dictionary { { "1", "11" } }), - new(new SonarCompositeRuleId("vbnet", "S2"), new Dictionary()), - new(new SonarCompositeRuleId("vbnet", "S3"), new Dictionary()), - new(new SonarCompositeRuleId("vbnet", "S4"), new Dictionary { { "4", "44" } }), - }); - } - - [TestMethod] - public void Update_GroupsRulesByLanguage() - { - IReadOnlyList fakeRoslynLanguages = [Language.VBNET, Language.CSharp]; - languageProvider.RoslynLanguages.Returns(fakeRoslynLanguages); - var rules = new Dictionary() - { - { "vbnet:S1", new RuleConfig(default) }, - { "vbnet:S2", new RuleConfig(default) }, - { "csharpsquid:S3", new RuleConfig(default) }, - { $"{FakeRoslynLanguage.Instance.RepoInfo.Key}:S4", new RuleConfig(default) }, - }; - - testSubject.Update(new UserSettings(new AnalysisSettings(rules, [], []), "any")); - - Received.InOrder(() => - { - roslynConfigGenerator - .GenerateAndSaveConfiguration( - Language.VBNET, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 2), - Arg.Is>(x => x.Count == 2)); - roslynConfigGenerator - .GenerateAndSaveConfiguration( - Language.CSharp, - Arg.Any(), - Arg.Is>(x => x.Count == 0), - Arg.Any(), - Arg.Is>(x => x.Count == 1), - Arg.Is>(x => x.Count == 1)); - }); - roslynConfigGenerator - .DidNotReceive() - .GenerateAndSaveConfiguration( - FakeRoslynLanguage.Instance, - Arg.Any(), - Arg.Any>(), - Arg.Any(), - Arg.Any>(), - Arg.Any>()); - } -} diff --git a/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs b/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs index 65437c118e..848997e1ab 100644 --- a/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs +++ b/src/Integration.Vsix/Analysis/IssueConsumerFactory.cs @@ -20,7 +20,6 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.IssueVisualization.Models; using SonarLint.VisualStudio.IssueVisualization.Security.Hotspots; @@ -42,7 +41,8 @@ internal interface IIssueConsumerFactory /// Instancing: a new issue consumer should be created for each analysis request /// i.e. the lifetime of the issue consumer should be tied to that analysis. /// - IIssueConsumer Create(ITextDocument textDocument, + IIssueConsumer Create( + ITextDocument textDocument, string analysisFilePath, ITextSnapshot analysisSnapshot, string projectName, @@ -64,7 +64,8 @@ internal IssueConsumerFactory(IAnalysisIssueVisualizationConverter converter, IL this.localHotspotsStore = localHotspotsStore; } - public IIssueConsumer Create(ITextDocument textDocument, + public IIssueConsumer Create( + ITextDocument textDocument, string analysisFilePath, ITextSnapshot analysisSnapshot, string projectName, diff --git a/src/Integration.Vsix/SonarLintDaemonPackage.cs b/src/Integration.Vsix/SonarLintDaemonPackage.cs index c82645b3d6..d2ab9af49a 100644 --- a/src/Integration.Vsix/SonarLintDaemonPackage.cs +++ b/src/Integration.Vsix/SonarLintDaemonPackage.cs @@ -27,7 +27,6 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.CFamily; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; using SonarLint.VisualStudio.Integration.CSharpVB.Install; using SonarLint.VisualStudio.Integration.Vsix.Analysis; using SonarLint.VisualStudio.Integration.Vsix.Events; @@ -68,7 +67,6 @@ public sealed class SonarLintDaemonPackage : AsyncPackage private ILogger logger; private IActiveCompilationDatabaseTracker activeCompilationDatabaseTracker; - private ISolutionRoslynAnalyzerManager solutionRoslynAnalyzerManager; private IProjectDocumentsEventsListener projectDocumentsEventsListener; private ISLCoreHandler slCoreHandler; private IDocumentEventsHandler documentEventsHandler; @@ -121,7 +119,6 @@ private async Task InitAsync() projectDocumentsEventsListener = await this.GetMefServiceAsync(); projectDocumentsEventsListener.Initialize(); - solutionRoslynAnalyzerManager = await this.GetMefServiceAsync(); var importBeforeFileGenerator = await this.GetMefServiceAsync(); importBeforeFileGenerator.UpdateOrCreateTargetsFileAsync().Forget(); @@ -166,8 +163,6 @@ protected override void Dispose(bool disposing) projectDocumentsEventsListener?.Dispose(); projectDocumentsEventsListener = null; - solutionRoslynAnalyzerManager?.Dispose(); - solutionRoslynAnalyzerManager = null; slCoreHandler?.Dispose(); slCoreHandler = null; diff --git a/src/Integration.Vsix/SonarLintIntegrationPackage.cs b/src/Integration.Vsix/SonarLintIntegrationPackage.cs index 21e7d1487d..a71554bc0c 100644 --- a/src/Integration.Vsix/SonarLintIntegrationPackage.cs +++ b/src/Integration.Vsix/SonarLintIntegrationPackage.cs @@ -26,7 +26,6 @@ using SonarLint.VisualStudio.Infrastructure.VS; using SonarLint.VisualStudio.Integration.Vsix.Resources; using SonarLint.VisualStudio.Integration.Vsix.Settings.FileExclusions; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; using ErrorHandler = Microsoft.VisualStudio.ErrorHandler; namespace SonarLint.VisualStudio.Integration.Vsix @@ -65,7 +64,6 @@ public class SonarLintIntegrationPackage : AsyncPackage private PackageCommandManager commandManager; private ILogger logger; - private IRoslynSettingsFileSynchronizer roslynSettingsFileSynchronizer; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { @@ -88,7 +86,6 @@ private async Task InitOnUIThreadAsync() await commandManager.InitializeAsync(this, ShowOptionPage); // make sure roslynSettingsFileSynchronizer is initialized - roslynSettingsFileSynchronizer = await this.GetMefServiceAsync(); Debug.Assert(threadHandling.CheckAccess(), "Still expecting to be on the UI thread"); logger.WriteLine(Strings.SL_InitializationComplete); @@ -99,15 +96,5 @@ private async Task InitOnUIThreadAsync() logger.WriteLine(Strings.SL_ERROR, ex.Message); } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - roslynSettingsFileSynchronizer?.Dispose(); - roslynSettingsFileSynchronizer = null; - } - } } } diff --git a/src/Integration/CSharpVB/RoslynConfigGenerator.cs b/src/Integration/CSharpVB/RoslynConfigGenerator.cs deleted file mode 100644 index e880cca159..0000000000 --- a/src/Integration/CSharpVB/RoslynConfigGenerator.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Core.SystemAbstractions; - -namespace SonarLint.VisualStudio.Integration.CSharpVB; - -[Export(typeof(IRoslynConfigGenerator))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -[ExcludeFromCodeCoverage] // todo https://sonarsource.atlassian.net/browse/SLVS-2420 -internal class RoslynConfigGenerator( - IFileSystemService fileSystem, - IGlobalConfigGenerator globalConfigGenerator, - ISonarLintConfigGenerator sonarLintConfigGenerator) - : IRoslynConfigGenerator -{ - /// - /// internal sonar-dotnet format used to provide rule parameters and file exclusions to the analyzer - /// - private const string SonarlintConfigFileName = "SonarLint.xml"; - - public void GenerateAndSaveConfiguration( - RoslynLanguage language, - string baseDirectory, - IDictionary properties, - IFileExclusions fileExclusions, - IReadOnlyCollection ruleStatuses, - IReadOnlyCollection ruleParameters) - { - var roslynGlobalConfig = globalConfigGenerator.Generate(ruleStatuses); - Save(roslynGlobalConfig, Path.Combine(baseDirectory, language.SettingsFileNameAndExtension)); - } - - private void Save(string config, string filePath) - { - EnsureParentDirectoryExists(filePath); - fileSystem.File.WriteAllText(filePath, config); - } - - private void EnsureParentDirectoryExists(string filePath) - { - var parentDirectory = Path.GetDirectoryName(filePath); - fileSystem.Directory.CreateDirectory(parentDirectory); // will no-op if exists - } -} diff --git a/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs b/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs index d657cde620..259c012490 100644 --- a/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs +++ b/src/Integration/CSharpVB/StandaloneMode/StandaloneRoslynSettingsUpdater.cs @@ -19,10 +19,7 @@ */ using System.ComponentModel.Composition; -using System.IO; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; +using System.Diagnostics.CodeAnalysis; using SonarLint.VisualStudio.Core.UserRuleSettings; namespace SonarLint.VisualStudio.Integration.CSharpVB.StandaloneMode; @@ -35,74 +32,12 @@ public interface IStandaloneRoslynSettingsUpdater [Export(typeof(IStandaloneRoslynSettingsUpdater))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class StandaloneRoslynSettingsUpdater( - IRoslynConfigGenerator generator, - ILanguageProvider languageProvider, - IThreadHandling threadHandling) +[ExcludeFromCodeCoverage] // todo https://sonarsource.atlassian.net/browse/SLVS-2420 +internal class StandaloneRoslynSettingsUpdater() : IStandaloneRoslynSettingsUpdater { - private readonly object lockObject = new(); - - public void Update(UserSettings settings) => - threadHandling - .RunOnBackgroundThread(() => UpdateInternal(settings)) - .Forget(); - - private void UpdateInternal(UserSettings settings) - { - lock (lockObject) - { - var exclusions = ConvertExclusions(settings); - var (ruleStatusesByLanguage, ruleParametersByLanguage) = ConvertRules(settings); - - foreach (var language in languageProvider.RoslynLanguages) - { - generator.GenerateAndSaveConfiguration( - language, - settings.BaseDirectory, - settings.AnalysisSettings.AnalysisProperties, - exclusions, - ruleStatusesByLanguage[language], - ruleParametersByLanguage[language]); - } - } - } - - private static StandaloneRoslynFileExclusions ConvertExclusions(UserSettings settings) + public void Update(UserSettings settings) { - var exclusions = new StandaloneRoslynFileExclusions(settings.AnalysisSettings); - return exclusions; - } - - private (Dictionary>, Dictionary>) ConvertRules(UserSettings settings) - { - var ruleStatusesByLanguage = InitializeForAllRoslynLanguages(); - var ruleParametersByLanguage = InitializeForAllRoslynLanguages(); - foreach (var analysisSettingsRule in settings.AnalysisSettings.Rules) - { - if (!SonarCompositeRuleId.TryParse(analysisSettingsRule.Key, out var ruleId) - || !languageProvider.RoslynLanguages.Contains(ruleId.Language)) - { - continue; - } - - ruleStatusesByLanguage[ruleId.Language] - .Add(new StandaloneRoslynRuleStatus(ruleId, analysisSettingsRule.Value.Level is RuleLevel.On)); - ruleParametersByLanguage[ruleId.Language] - .Add(new StandaloneRoslynRuleParameters(ruleId, analysisSettingsRule.Value.Parameters)); - } - return (ruleStatusesByLanguage, ruleParametersByLanguage); - } - - private Dictionary> InitializeForAllRoslynLanguages() - { - var dictionary = new Dictionary>(); - - foreach (var language in languageProvider.RoslynLanguages) - { - dictionary[language] = []; - } - - return dictionary; + // TODO by https://sonarsource.atlassian.net/browse/SLVS-2420 drop this class } } diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs deleted file mode 100644 index bda105d340..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/RoslynSettingsFileSynchronizerTests.cs +++ /dev/null @@ -1,405 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class RoslynSettingsFileSynchronizerTests -{ - private const string DefaultSln = "DefaultSolution"; - private IConfigurationProvider configProvider; - private ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory; - private IRoslynSettingsFileStorage roslynSettingsFileStorage; - private ISolutionInfoProvider solutionInfoProvider; - private RoslynSettingsFileSynchronizer testSubject; - private IThreadHandling threadHandling; - private readonly BindingConfiguration connectedBindingConfiguration = CreateConnectedConfiguration("some project key"); - private ISolutionBindingRepository solutionBindingRepository; - private IRoslynSuppressionUpdater roslynSuppressionUpdater; - private ISuppressedIssuesCalculator suppressedIssuesCalculator; - - private readonly SonarQubeIssue csharpIssueSuppressed = CreateSonarQubeIssue("csharpsquid:S111"); - private readonly SonarQubeIssue vbNetIssueSuppressed = CreateSonarQubeIssue("vbnet:S222"); - - [TestInitialize] - public void TestInitialize() - { - roslynSettingsFileStorage = Substitute.For(); - configProvider = Substitute.For(); - solutionInfoProvider = Substitute.For(); - solutionBindingRepository = Substitute.For(); - roslynSuppressionUpdater = Substitute.For(); - threadHandling = new NoOpThreadHandler(); - MockSuppressedIssuesCalculator(); - - testSubject = CreateTestSubject(threadHandling); - MockSolutionInfoProvider(DefaultSln); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - - [TestMethod] - public void Ctor_RegisterToEvents() - { - solutionBindingRepository.Received(1).BindingDeleted += Arg.Any>(); - - roslynSuppressionUpdater.Received(1).SuppressedIssuesReloaded += Arg.Any>(); - roslynSuppressionUpdater.Received(1).NewIssuesSuppressed += Arg.Any>(); - roslynSuppressionUpdater.Received(1).SuppressionsRemoved += Arg.Any>(); - roslynSuppressionUpdater.ReceivedCalls().Should().HaveCount(3); - } - - [TestMethod] - public void Dispose_UnregisterFromEvents() - { - testSubject.Dispose(); - - solutionBindingRepository.Received(1).BindingDeleted -= Arg.Any>(); - roslynSuppressionUpdater.Received(1).SuppressedIssuesReloaded -= Arg.Any>(); - roslynSuppressionUpdater.Received(1).NewIssuesSuppressed -= Arg.Any>(); - roslynSuppressionUpdater.Received(1).SuppressionsRemoved -= Arg.Any>(); - } - - [TestMethod] - public void BindingDeleted_StorageFileDeleted() - { - var localBindingKey = "my solution name"; - solutionBindingRepository.BindingDeleted += Raise.EventWith(new LocalBindingKeyEventArgs(localBindingKey)); - - roslynSettingsFileStorage.Received(1).Delete(localBindingKey); - } - - [TestMethod] - public void SuppressedIssuesReloaded_StandaloneMode_StorageFileDeleted() - { - MockConfigProvider(BindingConfiguration.Standalone); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - - RaiseSuppressedIssuesReloaded(sonarQubeIssues); - - configProvider.Received(1).GetConfiguration(); - roslynSettingsFileStorage.Received(1).Delete(DefaultSln); - roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] // should update storage even when there are no issues - public void SuppressedIssuesReloaded_ConnectedMode_StorageUpdated(bool hasIssues) - { - MockConfigProvider(connectedBindingConfiguration); - var issues = hasIssues ? new[] { csharpIssueSuppressed, vbNetIssueSuppressed } : Array.Empty(); - - RaiseSuppressedIssuesReloaded(issues); - - roslynSettingsFileStorage.Received(1).Update(Arg.Any(), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(issues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void SuppressedIssuesReloaded_FileStorageIsUpdatedOnBackgroundThread() - { - MockConfigProvider(connectedBindingConfiguration); - var threadHandlingMock = Substitute.For(); - CreateTestSubject(threadHandlingMock); - - RaiseSuppressedIssuesReloaded([csharpIssueSuppressed]); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); - } - - [TestMethod] - public void SuppressedIssuesReloaded_NoSolution_DoesNothing() - { - MockSolutionInfoProvider(null); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - - RaiseSuppressedIssuesReloaded(sonarQubeIssues); - - solutionInfoProvider.Received(1).GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void SuppressedIssuesReloaded_ConnectedMode_UpdatesFileStorage() - { - MockConfigProvider(connectedBindingConfiguration); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - var expectedSonarQubeIssues = new[] { CreateIssue(issueServerKey: csharpIssueSuppressed.IssueKey) }; - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns(expectedSonarQubeIssues); - - RaiseSuppressedIssuesReloaded(sonarQubeIssues); - - roslynSettingsFileStorage.Received(1) - .Update(Arg.Is(x => VerifyExpectedRoslynSettings(x, expectedSonarQubeIssues)), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void SuppressedIssuesReloaded_SuppressedIssueCalculatorReturnsNull_DoesNothing() - { - MockConfigProvider(connectedBindingConfiguration); - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns((IEnumerable)null); - - RaiseSuppressedIssuesReloaded([csharpIssueSuppressed, vbNetIssueSuppressed]); - - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - } - - [TestMethod] - public void NewIssuesSuppressed_StandaloneMode_StorageFileDeleted() - { - MockConfigProvider(BindingConfiguration.Standalone); - var newSonarQubeIssues = new[] { csharpIssueSuppressed }; - - RaiseNewIssuesSuppressed(newSonarQubeIssues); - - configProvider.Received(1).GetConfiguration(); - roslynSettingsFileStorage.Received(1).Delete(DefaultSln); - roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); - suppressedIssuesCalculatorFactory.Received(1).CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesSuppressed_NoIssues_StorageNotUpdated() - { - MockConfigProvider(connectedBindingConfiguration); - - RaiseNewIssuesSuppressed([]); - - roslynSettingsFileStorage.DidNotReceive().Update(Arg.Any(), DefaultSln); - suppressedIssuesCalculatorFactory.DidNotReceive().CreateNewSuppressedIssuesCalculator(Arg.Any()); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesSuppressed_FileStorageIsUpdatedOnBackgroundThread() - { - MockConfigProvider(connectedBindingConfiguration); - var threadHandlingMock = Substitute.For(); - CreateTestSubject(threadHandlingMock); - - RaiseNewIssuesSuppressed([csharpIssueSuppressed]); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); - } - - [TestMethod] - public void NewIssuesSuppressed_NoSolution_DoesNothing() - { - MockSolutionInfoProvider(null); - var newSonarQubeIssues = new[] { csharpIssueSuppressed }; - - RaiseNewIssuesSuppressed(newSonarQubeIssues); - - solutionInfoProvider.Received(1).GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.Received(1).CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesSuppressed_ConnectedMode_UpdatesFileStorage() - { - MockConfigProvider(connectedBindingConfiguration); - var sonarQubeIssues = new[] { csharpIssueSuppressed, vbNetIssueSuppressed }; - var expectedSonarQubeIssues = new[] { CreateIssue(issueServerKey: csharpIssueSuppressed.IssueKey) }; - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns(expectedSonarQubeIssues); - - RaiseNewIssuesSuppressed(sonarQubeIssues); - - roslynSettingsFileStorage.Received(1) - .Update(Arg.Is(x => VerifyExpectedRoslynSettings(x, expectedSonarQubeIssues)), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateNewSuppressedIssuesCalculator(sonarQubeIssues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void NewIssuesSuppressed_SuppressedIssueCalculatorReturnsNull_DoesNothing() - { - MockConfigProvider(connectedBindingConfiguration); - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns((IEnumerable)null); - - RaiseNewIssuesSuppressed([csharpIssueSuppressed, vbNetIssueSuppressed]); - - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - } - - [TestMethod] - public void NewIssuesResolved_StandaloneMode_StorageFileDeleted() - { - MockConfigProvider(BindingConfiguration.Standalone); - var newSonarQubeIssues = new[] { csharpIssueSuppressed.IssueKey }; - - RaiseSuppressionsRemoved(newSonarQubeIssues); - - configProvider.Received(1).GetConfiguration(); - roslynSettingsFileStorage.Received(1).Delete(Path.GetFileNameWithoutExtension(DefaultSln)); - roslynSettingsFileStorage.ReceivedCalls().Should().HaveCount(1); - suppressedIssuesCalculatorFactory.Received(1).CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesResolved_NoIssues_StorageNotUpdated() - { - MockConfigProvider(connectedBindingConfiguration); - - RaiseSuppressionsRemoved([]); - - solutionInfoProvider.DidNotReceive().GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.DidNotReceive().CreateSuppressedIssuesRemovedCalculator(Arg.Any()); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesResolved_FileStorageIsUpdatedOnBackgroundThread() - { - MockConfigProvider(connectedBindingConfiguration); - var threadHandlingMock = Substitute.For(); - CreateTestSubject(threadHandlingMock); - - RaiseSuppressionsRemoved([csharpIssueSuppressed.IssueKey]); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>()); - } - - [TestMethod] - public void NewIssuesResolved_NoSolution_DoesNothing() - { - MockSolutionInfoProvider(null); - var newSonarQubeIssues = new[] { csharpIssueSuppressed.IssueKey }; - - RaiseSuppressionsRemoved(newSonarQubeIssues); - - solutionInfoProvider.Received(1).GetSolutionNameAsync(); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - suppressedIssuesCalculatorFactory.Received().CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues); - suppressedIssuesCalculator.DidNotReceive().GetSuppressedIssuesOrNull(Arg.Any()); - } - - [TestMethod] - public void NewIssuesResolved_ConnectedMode_UpdatesFileStorage() - { - MockConfigProvider(connectedBindingConfiguration); - var sonarQubeIssues = new[] { csharpIssueSuppressed.IssueKey, vbNetIssueSuppressed.IssueKey }; - var expectedSonarQubeIssues = new[] { CreateIssue(issueServerKey: csharpIssueSuppressed.IssueKey) }; - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns(expectedSonarQubeIssues); - - RaiseSuppressionsRemoved(sonarQubeIssues); - - roslynSettingsFileStorage.Received(1) - .Update(Arg.Is(x => VerifyExpectedRoslynSettings(x, expectedSonarQubeIssues)), DefaultSln); - suppressedIssuesCalculatorFactory.Received(1).CreateSuppressedIssuesRemovedCalculator(sonarQubeIssues); - suppressedIssuesCalculator.Received(1).GetSuppressedIssuesOrNull(DefaultSln); - } - - [TestMethod] - public void NewIssuesResolved_SuppressedIssueCalculatorReturnsNull_DoesNothing() - { - MockConfigProvider(connectedBindingConfiguration); - suppressedIssuesCalculator.GetSuppressedIssuesOrNull(DefaultSln).Returns((IEnumerable)null); - - RaiseSuppressionsRemoved([csharpIssueSuppressed.IssueKey, vbNetIssueSuppressed.IssueKey]); - - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - roslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Delete(default); - } - - private void MockConfigProvider(BindingConfiguration configuration) => configProvider.GetConfiguration().Returns(configuration); - - private static BindingConfiguration CreateConnectedConfiguration(string projectKey) - { - var project = new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://bound"))); - - return BindingConfiguration.CreateBoundConfiguration(project, SonarLintMode.Connected, "some directory"); - } - - private void MockSolutionInfoProvider(string solutionName) => solutionInfoProvider.GetSolutionNameAsync().Returns(solutionName); - - private void RaiseSuppressedIssuesReloaded(SonarQubeIssue[] issues) => roslynSuppressionUpdater.SuppressedIssuesReloaded += Raise.EventWith(null, new SuppressionsEventArgs(issues)); - - private void RaiseNewIssuesSuppressed(SonarQubeIssue[] issues) => roslynSuppressionUpdater.NewIssuesSuppressed += Raise.EventWith(null, new SuppressionsEventArgs(issues)); - - private void RaiseSuppressionsRemoved(string[] issueKeys) => roslynSuppressionUpdater.SuppressionsRemoved += Raise.EventWith(null, new SuppressionsRemovedEventArgs(issueKeys)); - - private bool VerifyExpectedRoslynSettings(RoslynSettings roslynSettings, IEnumerable expectedSuppressedIssues) => - roslynSettings.SonarProjectKey == connectedBindingConfiguration.Project.ServerProjectKey - && roslynSettings.Suppressions.SequenceEqual(expectedSuppressedIssues); - - private void MockSuppressedIssuesCalculator() - { - suppressedIssuesCalculator = Substitute.For(); - suppressedIssuesCalculatorFactory = Substitute.For(); - suppressedIssuesCalculatorFactory.CreateAllSuppressedIssuesCalculator(Arg.Any()).Returns(suppressedIssuesCalculator); - suppressedIssuesCalculatorFactory.CreateNewSuppressedIssuesCalculator(Arg.Any()).Returns(suppressedIssuesCalculator); - suppressedIssuesCalculatorFactory.CreateSuppressedIssuesRemovedCalculator(Arg.Any()).Returns(suppressedIssuesCalculator); - } - - private RoslynSettingsFileSynchronizer CreateTestSubject(IThreadHandling mockedThreadHandling) => - new( - roslynSettingsFileStorage, - configProvider, - solutionInfoProvider, - solutionBindingRepository, - roslynSuppressionUpdater, - suppressedIssuesCalculatorFactory, - mockedThreadHandling); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs deleted file mode 100644 index df3e929ebc..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/RoslynSettingsFileSynchronizer.cs +++ /dev/null @@ -1,188 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.ConnectedMode.Suppressions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.ETW; -using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -/// -/// Responsible for listening to server suppression events and calling -/// with the new suppressions. -/// -public interface IRoslynSettingsFileSynchronizer : IDisposable -{ -} - -[Export(typeof(IRoslynSettingsFileSynchronizer))] -[PartCreationPolicy(CreationPolicy.NonShared)] // stateless - doesn't need to be shared -internal sealed class RoslynSettingsFileSynchronizer : IRoslynSettingsFileSynchronizer -{ - private readonly IConfigurationProvider configurationProvider; - private readonly ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory; - private readonly IRoslynSettingsFileStorage roslynSettingsFileStorage; - private readonly ISolutionInfoProvider solutionInfoProvider; - private readonly ISolutionBindingRepository solutionBindingRepository; - private readonly IRoslynSuppressionUpdater roslynSuppressionUpdater; - private readonly IThreadHandling threadHandling; - private readonly object lockObject = new(); - - [ImportingConstructor] - public RoslynSettingsFileSynchronizer( - IRoslynSettingsFileStorage roslynSettingsFileStorage, - IConfigurationProvider configurationProvider, - ISolutionInfoProvider solutionInfoProvider, - ISolutionBindingRepository solutionBindingRepository, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory) - : this(roslynSettingsFileStorage, - configurationProvider, - solutionInfoProvider, - solutionBindingRepository, - roslynSuppressionUpdater, - suppressedIssuesCalculatorFactory, - ThreadHandling.Instance) - { - } - - internal RoslynSettingsFileSynchronizer( - IRoslynSettingsFileStorage roslynSettingsFileStorage, - IConfigurationProvider configurationProvider, - ISolutionInfoProvider solutionInfoProvider, - ISolutionBindingRepository solutionBindingRepository, - IRoslynSuppressionUpdater roslynSuppressionUpdater, - ISuppressedIssuesCalculatorFactory suppressedIssuesCalculatorFactory, - IThreadHandling threadHandling) - { - this.roslynSettingsFileStorage = roslynSettingsFileStorage; - this.configurationProvider = configurationProvider; - this.solutionInfoProvider = solutionInfoProvider; - this.solutionBindingRepository = solutionBindingRepository; - this.roslynSuppressionUpdater = roslynSuppressionUpdater; - this.suppressedIssuesCalculatorFactory = suppressedIssuesCalculatorFactory; - this.threadHandling = threadHandling; - - this.roslynSuppressionUpdater.SuppressedIssuesReloaded += OnSuppressedIssuesReloaded; - this.roslynSuppressionUpdater.NewIssuesSuppressed += OnNewIssuesSuppressed; - this.roslynSuppressionUpdater.SuppressionsRemoved += OnRoslynSuppressionsRemoved; - solutionBindingRepository.BindingDeleted += OnBindingDeleted; - } - - private void OnBindingDeleted(object sender, LocalBindingKeyEventArgs e) => roslynSettingsFileStorage.Delete(e.LocalBindingKey); - - public void Dispose() - { - solutionBindingRepository.BindingDeleted -= OnBindingDeleted; - roslynSuppressionUpdater.SuppressedIssuesReloaded -= OnSuppressedIssuesReloaded; - roslynSuppressionUpdater.NewIssuesSuppressed -= OnNewIssuesSuppressed; - roslynSuppressionUpdater.SuppressionsRemoved -= OnRoslynSuppressionsRemoved; - } - - private void OnSuppressedIssuesReloaded(object sender, SuppressionsEventArgs e) => - UpdateFileStorageAsync(suppressedIssuesCalculatorFactory.CreateAllSuppressedIssuesCalculator(e.SuppressedIssues)).Forget(); - - private void OnNewIssuesSuppressed(object sender, SuppressionsEventArgs e) - { - if (!e.SuppressedIssues.Any()) - { - return; - } - - UpdateFileStorageAsync(suppressedIssuesCalculatorFactory.CreateNewSuppressedIssuesCalculator(e.SuppressedIssues)).Forget(); - } - - private void OnRoslynSuppressionsRemoved(object sender, SuppressionsRemovedEventArgs e) - { - if (!e.IssueServerKeys.Any()) - { - return; - } - - UpdateFileStorageAsync(suppressedIssuesCalculatorFactory.CreateSuppressedIssuesRemovedCalculator(e.IssueServerKeys)).Forget(); - } - - /// - /// Updates the Roslyn suppressed issues file if in connected mode - /// - private async Task UpdateFileStorageAsync(ISuppressedIssuesCalculator suppressedIssuesCalculator) => - await threadHandling.RunOnBackgroundThread(async () => - { - await UpdateFileStorageIfNeededAsync(suppressedIssuesCalculator); - return true; - }); - - private async Task UpdateFileStorageIfNeededAsync(ISuppressedIssuesCalculator suppressedIssuesCalculator) - { - CodeMarkers.Instance.FileSynchronizerUpdateStart(); - try - { - var solutionNameWithoutExtension = await solutionInfoProvider.GetSolutionNameAsync(); - if (string.IsNullOrEmpty(solutionNameWithoutExtension)) - { - return; - } - - var sonarProjectKey = configurationProvider.GetConfiguration().Project?.ServerProjectKey; - if (string.IsNullOrEmpty(sonarProjectKey)) - { - SafeDeleteRoslynSettingsFileStorage(solutionNameWithoutExtension); - return; - } - - SafeUpdateRoslynSettingsFileStorage(suppressedIssuesCalculator, solutionNameWithoutExtension, sonarProjectKey); - } - finally - { - CodeMarkers.Instance.FileSynchronizerUpdateStop(); - } - } - - private void SafeDeleteRoslynSettingsFileStorage(string solutionNameWithoutExtension) - { - lock (lockObject) - { - roslynSettingsFileStorage.Delete(solutionNameWithoutExtension); - } - } - - private void SafeUpdateRoslynSettingsFileStorage( - ISuppressedIssuesCalculator suppressedIssuesCalculator, - string solutionNameWithoutExtension, - string sonarProjectKey) - { - lock (lockObject) - { - var suppressionsToAdd = suppressedIssuesCalculator.GetSuppressedIssuesOrNull(solutionNameWithoutExtension); - if (suppressionsToAdd == null) - { - return; - } - var roslynSettings = new RoslynSettings { SonarProjectKey = sonarProjectKey, Suppressions = suppressionsToAdd }; - roslynSettingsFileStorage.Update(roslynSettings, solutionNameWithoutExtension); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs deleted file mode 100644 index 74da9bc20d..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetAllPropertiesAsync.cs +++ /dev/null @@ -1,219 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetAllPropertiesAsync : SonarQubeService_TestBase - { - [TestMethod] - public async Task GetProperties_Old_ExampleFromSonarQube() - { - await ConnectToSonarQube(); - - SetupRequest("api/properties?resource=my-project", @"[ - { - ""key"": ""sonar.test.jira"", - ""value"": ""abc"" - }, - { - ""key"": ""sonar.autogenerated"", - ""value"": ""val1,val2,val3"", - ""values"": [ - ""val1"", - ""val2"", - ""val3"" - ] - }, - { - ""key"": ""sonar.demo"", - ""value"": ""1,2"", - ""values"": [ - ""1"", - ""2"" - ] - }, - { - ""key"": ""sonar.demo.1.text"", - ""value"": ""foo"" - }, - { - ""key"": ""sonar.demo.1.boolean"", - ""value"": ""true"" - }, - { - ""key"": ""sonar.demo.2.text"", - ""value"": ""bar"" - }, - { - ""key"": ""sonar.demo.2.boolean"", - ""value"": ""false"" - } -]"); - - var result = await service.GetAllPropertiesAsync("my-project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(7); - result.Select(x => x.Key).Should().BeEquivalentTo( - new[] { "sonar.test.jira", "sonar.autogenerated", "sonar.demo", "sonar.demo.1.text", "sonar.demo.1.boolean", "sonar.demo.2.text", "sonar.demo.2.boolean" }); - result.Select(x => x.Value).Should().BeEquivalentTo( - new[] { "abc", "val1,val2,val3", "1,2", "foo", "true", "bar", "false" }); - } - - [TestMethod] - public async Task GetProperties_Old_NotFound() - { - await ConnectToSonarQube(); - - SetupRequest("api/properties", "", HttpStatusCode.NotFound); - - Func>> func = async () => - await service.GetAllPropertiesAsync(null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetProperties_ExampleFromSonarQube() - { - await ConnectToSonarQube("6.3.0.0"); - - SetupRequest("api/settings/values?component=my-project", @"{ - ""settings"": [ - { - ""key"": ""sonar.test.jira"", - ""value"": ""abc"", - ""inherited"": true - }, - { - ""key"": ""sonar.autogenerated"", - ""values"": [ - ""val1"", - ""val2"", - ""val3"" - ], - ""inherited"": false - }, - { - ""key"": ""sonar.demo"", - ""fieldValues"": [ - { - ""boolean"": ""true"", - ""text"": ""foo"" - }, - { - ""boolean"": ""false"", - ""text"": ""bar"" - } - ], - ""inherited"": false - } - ] -}"); - - var result = await service.GetAllPropertiesAsync("my-project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(6); - result.Select(x => x.Key).Should().ContainInOrder( - new[] { "sonar.test.jira", "sonar.autogenerated", "sonar.demo.1.boolean", "sonar.demo.1.text", "sonar.demo.2.boolean", "sonar.demo.2.text", }); - - result.Select(x => x.Value).Should().ContainInOrder( - new[] { "abc", "val1,val2,val3", "true", "foo", "false", "bar" }); - } - - [TestMethod] - public async Task GetProperties_ExampleFromSonarQube_Project_NotFound() - { - await ConnectToSonarQube("6.3.0.0"); - - SetupRequest("api/settings/values?component=my-project", "", HttpStatusCode.NotFound); - - SetupRequest("api/settings/values", @"{ - ""settings"": [ - { - ""key"": ""sonar.test.jira"", - ""value"": ""abc"", - ""inherited"": true - }, - { - ""key"": ""sonar.autogenerated"", - ""values"": [ - ""val1"", - ""val2"", - ""val3"" - ], - ""inherited"": false - }, - { - ""key"": ""sonar.demo"", - ""fieldValues"": [ - { - ""boolean"": ""true"", - ""text"": ""foo"" - }, - { - ""boolean"": ""false"", - ""text"": ""bar"" - } - ], - ""inherited"": false - } - ] -}"); - - var result = await service.GetAllPropertiesAsync("my-project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(6); - result.Select(x => x.Key).Should().ContainInOrder( - new[] { "sonar.test.jira", "sonar.autogenerated", "sonar.demo.1.boolean", "sonar.demo.1.text", "sonar.demo.2.boolean", "sonar.demo.2.text", }); - - result.Select(x => x.Value).Should().ContainInOrder( - new[] { "abc", "val1,val2,val3", "true", "foo", "false", "bar" }); - } - - [TestMethod] - public void GetProperties_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetAllPropertiesAsync(null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs deleted file mode 100644 index dd008c3f5b..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetAllQualityProfilesAsync.cs +++ /dev/null @@ -1,324 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests; - -[TestClass] -public class SonarQubeService_GetAllQualityProfilesAsync : SonarQubeService_TestBase -{ - [TestMethod] - public async Task GetAllQualityProfilesAsync_Old_ExampleFromSonarQube() - { - await ConnectToSonarQube(); - - SetupRequest("api/qualityprofiles/search?projectKey=my_project&organization=my_organization", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcA-iU5OvuD2FL1"", - ""name"": ""My BU Profile"", - ""language"": ""java"", - ""languageName"": ""Java"", - ""isInherited"": true, - ""isBuiltIn"": false, - ""parentKey"": ""iU5OvuD2FLz"", - ""parentName"": ""My Company Profile"", - ""activeRuleCount"": 15, - ""activeDeprecatedRuleCount"": 5, - ""isDefault"": false, - ""projectCount"": 7, - ""rulesUpdatedAt"": ""2016-12-20T19:10:03+0100"", - ""lastUsed"": ""2016-12-21T16:10:03+0100"", - ""userUpdatedAt"": ""2016-06-28T21:57:01+0200"", - ""actions"": { - ""edit"": true, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcB-iU5OvuD2FL7"", - ""name"": ""Sonar way"", - ""language"": ""py"", - ""languageName"": ""Python"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 2, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2014-12-22T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None); - - httpClientHandler.VerifyAll(); - result.Select(x => x.Key).Should().BeEquivalentTo( - new[] { "AU-TpxcA-iU5OvuD2FL3", "AU-TpxcA-iU5OvuD2FL1", "AU-TpxcB-iU5OvuD2FL7" }); - result.Select(x => x.TimeStamp).Should().BeEquivalentTo(new[] { "2016-12-22T19:10:03+0100", "2016-12-20T19:10:03+0100", "2014-12-22T19:10:03+0100" } - .Select(DateTime.Parse)); - } - - [TestMethod] - public async Task GetAllQualityProfilesAsync_Old_ProjectNotAnalyzed() - { - await ConnectToSonarQube(); - - SetupRequest("api/qualityprofiles/search?projectKey=my_project&organization=my_organization", "", - HttpStatusCode.NotFound); - - SetupRequest("api/qualityprofiles/search?organization=my_organization&defaults=true", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = (await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None)) - .Single(); - - httpClientHandler.VerifyAll(); - - result.Should().NotBeNull(); - result.IsDefault.Should().BeTrue(); - result.Key.Should().Be("AU-TpxcA-iU5OvuD2FL3"); - result.Language.Should().Be("cs"); - result.Name.Should().Be("Sonar way"); - result.TimeStamp.Should().Be(DateTime.Parse("2016-12-22T19:10:03+0100")); - } - - [TestMethod] - public async Task GetAllQualityProfilesAsync_Old_Error() - { - await ConnectToSonarQube(); - - SetupRequest("api/qualityprofiles/search?projectKey=my_project&organization=my_organization", "", - HttpStatusCode.InternalServerError); - - Func>> func = async () => - await service.GetAllQualityProfilesAsync("my_project", "my_organization", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 500 (Internal Server Error)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetAllQualityProfiles_New_ExampleFromSonarQube() - { - await ConnectToSonarQube("6.5.0.0"); - - SetupRequest("api/qualityprofiles/search?project=my_project&organization=my_organization", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcA-iU5OvuD2FL1"", - ""name"": ""My BU Profile"", - ""language"": ""java"", - ""languageName"": ""Java"", - ""isInherited"": true, - ""isBuiltIn"": false, - ""parentKey"": ""iU5OvuD2FLz"", - ""parentName"": ""My Company Profile"", - ""activeRuleCount"": 15, - ""activeDeprecatedRuleCount"": 5, - ""isDefault"": false, - ""projectCount"": 7, - ""rulesUpdatedAt"": ""2016-12-20T19:10:03+0100"", - ""lastUsed"": ""2016-12-21T16:10:03+0100"", - ""userUpdatedAt"": ""2016-06-28T21:57:01+0200"", - ""actions"": { - ""edit"": true, - ""setAsDefault"": false, - ""copy"": false - } - }, - { - ""key"": ""AU-TpxcB-iU5OvuD2FL7"", - ""name"": ""Sonar way"", - ""language"": ""py"", - ""languageName"": ""Python"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 2, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2014-12-22T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None); - - httpClientHandler.VerifyAll(); - result.Select(x => x.Key).Should().BeEquivalentTo( - new[] { "AU-TpxcA-iU5OvuD2FL3", "AU-TpxcA-iU5OvuD2FL1", "AU-TpxcB-iU5OvuD2FL7" }); - result.Select(x => x.TimeStamp).Should().BeEquivalentTo( - new[] { "2016-12-22T19:10:03+0100", "2016-12-20T19:10:03+0100", "2014-12-22T19:10:03+0100" } - .Select(DateTime.Parse)); - - // Regression test for #1450 - shouldn't get a warning about max items retrieved - // https://github.com/SonarSource/sonarlint-visualstudio/issues/1450 - logger.WarningMessages.Count.Should().Be(0); - } - - [TestMethod] - public async Task GetAllQualityProfiles_New_ProjectNotAnalyzed() - { - await ConnectToSonarQube("6.5.0.0"); - - SetupRequest("api/qualityprofiles/search?project=my_project&organization=my_organization", "", - HttpStatusCode.NotFound); - - SetupRequest("api/qualityprofiles/search?organization=my_organization&defaults=true", @"{ - ""profiles"": [ - { - ""key"": ""AU-TpxcA-iU5OvuD2FL3"", - ""name"": ""Sonar way"", - ""language"": ""cs"", - ""languageName"": ""C#"", - ""isInherited"": false, - ""isBuiltIn"": true, - ""activeRuleCount"": 3, - ""activeDeprecatedRuleCount"": 0, - ""isDefault"": true, - ""rulesUpdatedAt"": ""2016-12-22T19:10:03+0100"", - ""lastUsed"": ""2016-12-01T19:10:03+0100"", - ""actions"": { - ""edit"": false, - ""setAsDefault"": false, - ""copy"": false - } - } - ], - ""actions"": { - ""create"": false - } -}"); - - var result = (await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None)) - .Single(); - - result.IsDefault.Should().BeTrue(); - result.Key.Should().Be("AU-TpxcA-iU5OvuD2FL3"); - result.Language.Should().Be("cs"); - result.Name.Should().Be("Sonar way"); - result.TimeStamp.Should().Be(DateTime.Parse("2016-12-22T19:10:03+0100")); - } - - [TestMethod] - public void GetAllQualityProfiles_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - var func = new Func(async () => await service.GetAllQualityProfilesAsync("my_project", - "my_organization", - CancellationToken.None)); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs deleted file mode 100644 index f601ebfe15..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetExclusionsRequest.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetExclusionsRequest : SonarQubeService_TestBase - { - [TestMethod] - public void Get_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func> func = async () => - await service.GetServerExclusions("any", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task Get_NotFound() - { - await ConnectToSonarQube("7.2.0.0"); - - const string request = "api/settings/values?component=my_project&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - - SetupRequest(request, "", HttpStatusCode.NotFound); - - Func> func = async () => - await service.GetServerExclusions("my_project", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task Get_V7_2_ExampleFromSonarQube() - { - await ConnectToSonarQube("7.2.0.0"); - - const string response = "{\"settings\":[{\"key\":\"sonar.global.exclusions\",\"values\":[\"**/build-wrapper-dump.json\"],\"inherited\":true}]}"; - const string request = "api/settings/values?component=my_project&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - - SetupRequest(request, response); - - var result = await service.GetServerExclusions("my_project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Inclusions.Should().BeEmpty(); - result.Exclusions.Should().BeEmpty(); - result.GlobalExclusions.Should().BeEquivalentTo("**/build-wrapper-dump.json"); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs deleted file mode 100644 index 2296e4404b..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesForComponent.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetIssuesForComponent : SonarQubeService_GetIssuesBase - { - [TestMethod] - public void GetIssuesForComponentAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetIssuesForComponentAsync("simplcom", null, null, null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task GetIssuesForComponentAsync_Connected_ReturnsExpected() - { - await ConnectToSonarQube("9.9.0.0"); - - string projectKey = "projectKey"; - string branch = "branch"; - string componentKey = "componentKey"; - string ruleId = "ruleId"; - - SetupPageOfResponses(projectKey, ruleId, componentKey, branch, 1, 1, "CODE_SMELL"); - SetupPageOfResponses(projectKey, ruleId, componentKey, branch, 1, 1, "BUG"); - - var result = await service.GetIssuesForComponentAsync(projectKey, branch, componentKey, ruleId, CancellationToken.None); - - result.Should().HaveCount(2); - } - - [TestMethod] - public async Task GetIssuesForComponentAsync_MaxIssues_ShowsLog() - { - await ConnectToSonarQube("9.9.0.0"); - - string projectKey = "projectKey"; - string branch = "branch"; - string componentKey = "componentKey"; - string ruleId = "ruleId"; - - SetupPagesOfResponses(projectKey, ruleId, componentKey, branch, MaxAllowedIssues, "CODE_SMELL"); - SetupPagesOfResponses(projectKey, ruleId, componentKey, branch, MaxAllowedIssues, "BUG"); - - var result = await service.GetIssuesForComponentAsync(projectKey, branch, componentKey, ruleId, CancellationToken.None); - - result.Should().HaveCount(MaxAllowedIssues * 2); - - DumpWarningsToConsole(); - - httpClientHandler.VerifyAll(); - - checkForExpectedWarning(MaxAllowedIssues, "code smells"); - checkForExpectedWarning(MaxAllowedIssues, "bugs"); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs deleted file mode 100644 index 7b86441e5c..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetRulesAsync.cs +++ /dev/null @@ -1,453 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetRulesAsync : SonarQubeService_TestBase - { - [TestMethod] - public async Task GetRulesAsync_Old_Active_SonarQubeResponse() - { - await ConnectToSonarQube(); - - SetupRequest("api/rules/search?activation=true&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"" - }, - ], - ""actives"": { - ""csharpsquid:S2225"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MAJOR"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S4524"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""CRITICAL"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S2342"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MINOR"", - ""params"": [ - { - ""key"": ""format"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"" - } - ], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ] - }, - ""qProfiles"": { - ""quality-profile-1"": { - ""name"": ""Sonar way"", - ""lang"": ""cs"", - ""langName"": ""C#"" - } - } -} -"); - - var result = await service.GetRulesAsync(true, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().Contain(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().Contain(new[] { true, true, true }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Major, SonarQubeIssueSeverity.Critical, SonarQubeIssueSeverity.Minor }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - result.Select(r => r.Parameters.Count).Should().Contain(new[] { 0, 2, 0 }); - result.SelectMany(r => r.Parameters.Select(p => p.Key)).Should().ContainInOrder(new[] { "format", "flagsAttributeFormat" }); - result.SelectMany(r => r.Parameters.Select(p => p.Value)).Should().ContainInOrder(new[] { "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$" }); - - // All rules with empty parameters should return the same (read-only) object - // 0 = S2225, no params; 1 = S4524, no params; 2 = S2342, has params - result[0].Parameters.Should().NotBeNull(); - result[0].Parameters.Count().Should().Be(0); - result[2].Parameters.Count().Should().Be(2); - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - } - - [TestMethod] - public async Task GetRulesAsync_9_6_Active_SonarQubeResponse() - { - await ConnectToSonarQube(version: "9.6.0.0"); - - SetupRequest("api/rules/search?activation=true&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"" - }, - ], - ""actives"": { - ""csharpsquid:S2225"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MAJOR"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S4524"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""CRITICAL"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S2342"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MINOR"", - ""params"": [ - { - ""key"": ""format"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"" - } - ], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ] - }, - ""qProfiles"": { - ""quality-profile-1"": { - ""name"": ""Sonar way"", - ""lang"": ""cs"", - ""langName"": ""C#"" - } - } -} -"); - - var result = await service.GetRulesAsync(true, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().Contain(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().Contain(new[] { true, true, true }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Major, SonarQubeIssueSeverity.Critical, SonarQubeIssueSeverity.Minor }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - result.Select(r => r.Parameters.Count).Should().Contain(new[] { 0, 2, 0 }); - result.SelectMany(r => r.Parameters.Select(p => p.Key)).Should().ContainInOrder(new[] { "format", "flagsAttributeFormat" }); - result.SelectMany(r => r.Parameters.Select(p => p.Value)).Should().ContainInOrder(new[] { "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$" }); - - // All rules with empty parameters should return the same (read-only) object - // 0 = S2225, no params; 1 = S4524, no params; 2 = S2342, has params - result[0].Parameters.Should().NotBeNull(); - result[0].Parameters.Count().Should().Be(0); - result[2].Parameters.Count().Should().Be(2); - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - } - - [TestMethod] - public async Task GetRulesAsync_9_5_Active_SonarQubeResponse() - { - await ConnectToSonarQube(version: "9.5.0.0"); - - SetupRequest("api/rules/search?activation=true&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"", - }, - ], - ""actives"": { - ""csharpsquid:S2225"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MAJOR"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S4524"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""CRITICAL"", - ""params"": [], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ], - ""csharpsquid:S2342"": [ - { - ""qProfile"": ""quality-profile-1"", - ""inherit"": ""NONE"", - ""severity"": ""MINOR"", - ""params"": [ - { - ""key"": ""format"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""value"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"" - } - ], - ""createdAt"": ""2019-01-10T14:00:14+0100"", - ""updatedAt"": ""2019-01-10T14:00:14+0100"" - } - ] - }, - ""qProfiles"": { - ""quality-profile-1"": { - ""name"": ""Sonar way"", - ""lang"": ""cs"", - ""langName"": ""C#"" - } - } -} -"); - - var result = await service.GetRulesAsync(true, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().Contain(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().Contain(new[] { true, true, true }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Major, SonarQubeIssueSeverity.Critical, SonarQubeIssueSeverity.Minor }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - result.Select(r => r.Parameters.Count).Should().Contain(new[] { 0, 2, 0 }); - result.SelectMany(r => r.Parameters.Select(p => p.Key)).Should().ContainInOrder(new[] { "format", "flagsAttributeFormat" }); - result.SelectMany(r => r.Parameters.Select(p => p.Value)).Should().ContainInOrder(new[] { "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$" }); - - // All rules with empty parameters should return the same (read-only) object - // 0 = S2225, no params; 1 = S4524, no params; 2 = S2342, has params - result[0].Parameters.Should().NotBeNull(); - result[0].Parameters.Count().Should().Be(0); - result[2].Parameters.Count().Should().Be(2); - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - } - - [TestMethod] - public async Task GetRulesAsync_NotActive_SonarQubeResponse() - { - await ConnectToSonarQube(); - - SetupRequest("api/rules/search?activation=false&qprofile=quality-profile-1&f=repo%2CinternalKey%2Cparams%2Cactives&p=1&ps=500", @" -{ - ""total"": 4, - ""p"": 1, - ""ps"": 500, - ""rules"": [ - { - ""key"": ""csharpsquid:S2225"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""BUG"" - }, - { - ""key"": ""csharpsquid:S4524"", - ""repo"": ""csharpsquid"", - ""params"": [], - ""type"": ""VULNERABILITY"" - }, - { - ""key"": ""csharpsquid:S2342"", - ""repo"": ""csharpsquid"", - ""params"": [ - { - ""key"": ""format"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"", - ""type"": ""STRING"" - }, - { - ""key"": ""flagsAttributeFormat"", - ""defaultValue"": ""^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"", - ""type"": ""STRING"" - } - ], - ""type"": ""CODE_SMELL"" - }, - ], - ""actives"": { }, - ""qProfiles"": { } -} -"); - - var result = await service.GetRulesAsync(false, "quality-profile-1", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(3); - result.Select(r => r.Key).Should().ContainInOrder(new[] { "S2225", "S4524", "S2342" }); - result.Select(r => r.RepositoryKey).Should().ContainInOrder(new[] { "csharpsquid", "csharpsquid", "csharpsquid" }); - result.Select(r => r.IsActive).Should().ContainInOrder(new[] { false, false, false }); - result.Select(r => r.Severity).Should().ContainInOrder(new[] { SonarQubeIssueSeverity.Unknown, SonarQubeIssueSeverity.Unknown, SonarQubeIssueSeverity.Unknown }); - result.Select(r => r.IssueType).Should().ContainInOrder(new[] { SonarQubeIssueType.Bug, SonarQubeIssueType.Vulnerability, SonarQubeIssueType.CodeSmell }); - - // The response contains parameter "definitions", the Parameters property contains parameter values - result.Select(r => r.Parameters.Count).Should().ContainInOrder(new[] { 0, 0, 0 }); - - // All empty parameter objects should be the same instance - result[0].Parameters.Should().BeSameAs(result[1].Parameters); - result[0].Parameters.Should().BeSameAs(result[2].Parameters); - } - - [TestMethod] - public void GetRulesAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetRulesAsync(true, "whatever", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs deleted file mode 100644 index 3fee3df968..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetSuppressedRoslynIssuesAsync.cs +++ /dev/null @@ -1,297 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarQube.Client.Models; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests; - -[TestClass] -public class SonarQubeService_GetSuppressedRoslynIssuesAsync : SonarQubeService_GetIssuesBase -{ - protected override RoslynLanguage[] MockRoslynLanguages => [Language.CSharp, Language.VBNET]; - private string[] MockRoslynServerLanguageKeys => MockRoslynLanguages.Select(x => x.ServerLanguageKey).ToArray(); - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20() - { - await ConnectToSonarQube("7.2.0.0"); - - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet&p=1&ps=500", @" -{ - ""total"": 5, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 5 - }, - ""issues"": [ - { - ""key"": ""AWg8bjfdFPFMeKWzHZ_7"", - ""rule"": ""csharpsquid:S3990"", - ""severity"": ""MAJOR"", - ""component"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"", - ""project"": ""shared"", - ""flows"": [], - ""resolution"": ""WONTFIX"", - ""status"": ""RESOLVED"", - ""message"": ""Mark this assembly with 'System.CLSCompliantAttribute'"", - ""effort"": ""1min"", - ""debt"": ""1min"", - ""author"": """", - ""tags"": [ - ""api-design"" - ], - ""creationDate"": ""2019-01-11T11:21:20+0100"", - ""updateDate"": ""2019-01-11T11:28:22+0100"", - ""type"": ""CODE_SMELL"", - ""organization"": ""default-organization"" - } - ], - ""components"": [ ] -} -"); - SetupRequest("api/issues/search?projects=shared&statuses=RESOLVED&types=BUG&languages=cs%2Cvbnet&p=1&ps=500", @" -{ - ""total"": 5, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 5 - }, - ""issues"": [ - { - ""key"": ""AWg8adcV_JurIR2zdSvR"", - ""rule"": ""csharpsquid:S1118"", - ""severity"": ""MAJOR"", - ""component"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs"", - ""project"": ""shared"", - ""subProject"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"", - ""line"": 6, - ""hash"": ""0afa1b5e62aa3cfaf1cd9a4e63571cb5"", - ""textRange"": { - ""startLine"": 6, - ""endLine"": 6, - ""startOffset"": 10, - ""endOffset"": 17 - }, - ""flows"": [], - ""resolution"": ""WONTFIX"", - ""status"": ""RESOLVED"", - ""message"": ""Add a 'protected' constructor or the 'static' keyword to the class declaration."", - ""effort"": ""10min"", - ""debt"": ""10min"", - ""author"": """", - ""tags"": [ - ""design"" - ], - ""creationDate"": ""2019-01-11T11:16:30+0100"", - ""updateDate"": ""2019-01-11T11:26:39+0100"", - ""type"": ""BUG"", - ""organization"": ""default-organization"" - } - ], - ""components"": [ - { - ""organization"": ""default-organization"", - ""key"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs"", - ""uuid"": ""AWg8adNk_JurIR2zdSvM"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Program.cs"", - ""longName"": ""Program.cs"", - ""path"": ""Program.cs"" - } - ] -} -"); - - var result = await service.GetSuppressedRoslynIssuesAsync("shared", null, null, CancellationToken.None); - - result.Should().HaveCount(2); - - // Module level issues don't have FilePath, hash and line - result[0].FilePath.Should().Be(string.Empty); - result[0].Hash.Should().BeNull(); - result[0].TextRange.Should().BeNull(); - result[0].Message.Should().Be("Mark this assembly with 'System.CLSCompliantAttribute'"); - result[0].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); - result[0].IsResolved.Should().BeTrue(); - result[0].RuleId.Should().Be("csharpsquid:S3990"); - result[0].Severity.Should().Be(SonarQubeIssueSeverity.Major); - - result[1].FilePath.Should().Be("Program.cs"); - result[1].Hash.Should().Be("0afa1b5e62aa3cfaf1cd9a4e63571cb5"); - result[1].TextRange.Should().BeEquivalentTo(new IssueTextRange(6, 6, 10, 17)); - result[1].Message.Should().Be("Add a 'protected' constructor or the 'static' keyword to the class declaration."); - result[1].ModuleKey.Should().Be("shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082"); - result[1].IsResolved.Should().BeTrue(); - result[1].RuleId.Should().Be("csharpsquid:S1118"); - result[1].Severity.Should().Be(SonarQubeIssueSeverity.Major); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_NotFound() - { - await ConnectToSonarQube("7.2.0.0"); - - SetupRequest("api/issues/search?projects=project1&statuses=RESOLVED&types=CODE_SMELL&languages=cs%2Cvbnet&p=1&ps=500", "", HttpStatusCode.NotFound); - - Func>> func = async () => - await service.GetSuppressedRoslynIssuesAsync("project1", null, null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_Paging() - { - await ConnectToSonarQube("7.2.0.0"); - - SetupPagesOfResponses("simplcom", 1001, "CODE_SMELL", MockRoslynServerLanguageKeys); - SetupPageOfResponses("simplcom", 1, 0, "BUG", MockRoslynServerLanguageKeys); - - var result = await service.GetSuppressedRoslynIssuesAsync("simplcom", null, null, CancellationToken.None); - - result.Should().HaveCount(1001); - result.Select(i => i.FilePath).Should().Match(paths => paths.All(p => p == "Program.cs")); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - // Note: we're not testing all possible combinations because testing with the - // max number of items is relatively slow (several seconds per iteration) - [DataRow(5, 5)] // No issue types with too many issues - [DataRow(MaxAllowedIssues, 5)] // One issue type with too many issues - [DataRow(1, MaxAllowedIssues)] // Multiple issue types with too many issues - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_NotifyWhenMaxIssuesReturned( - int numCodeSmells, - int numBugs) - { - await ConnectToSonarQube("7.2.0.0"); - - SetupPagesOfResponses("proj1", numCodeSmells, "CODE_SMELL", MockRoslynServerLanguageKeys); - SetupPagesOfResponses("proj1", numBugs, "BUG", MockRoslynServerLanguageKeys); - - var result = await service.GetSuppressedRoslynIssuesAsync("proj1", null, null, CancellationToken.None); - - result.Should().HaveCount( - Math.Min(MaxAllowedIssues, numCodeSmells) + - Math.Min(MaxAllowedIssues, numBugs)); - - DumpWarningsToConsole(); - - httpClientHandler.VerifyAll(); - - checkForExpectedWarning(numCodeSmells, "code smells"); - checkForExpectedWarning(numBugs, "bugs"); - } - - [TestMethod] - [DataRow("")] - [DataRow(null)] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", emptyBranch, null, CancellationToken.None); - - // Branch is null/empty => should not be passed - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(2); - actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("branch")); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_BranchIsSpecified_BranchIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", "aBranch", null, CancellationToken.None); - - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(2); - actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("&branch=aBranch&")); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_IssueKeysAreNotSpecified_IssueKeysAreNotIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", null, null, CancellationToken.None); - - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().HaveCount(2); - actualRequests.Should().NotContain(x => x.RequestUri.Query.Contains("issues")); - } - - [TestMethod] - public async Task GetSuppressedRoslynIssuesAsync_From_7_20_IssueKeysAreSpecified_IssueKeysAreIncludedInQueryString() - { - await ConnectToSonarQube("7.2.0.0"); - httpClientHandler.Reset(); - - SetupHttpRequest(httpClientHandler, EmptyGetIssuesResponse); - _ = await service.GetSuppressedRoslynIssuesAsync("any", null, new[] { "issue1", "issue2" }, CancellationToken.None); - - // The wrapper is expected to make one call with the given issueKeys - var actualRequests = httpClientHandler.GetSendAsyncRequests(); - actualRequests.Should().ContainSingle(); - actualRequests.Should().OnlyContain(x => x.RequestUri.Query.Contains("issues=issue1%2Cissue2")); - } - - [TestMethod] - public void GetSuppressedRoslynIssuesAsync_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetSuppressedRoslynIssuesAsync("simplcom", null, null, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } -} diff --git a/src/SonarQube.Client/ISonarQubeService.cs b/src/SonarQube.Client/ISonarQubeService.cs index 9e6bd9b151..d74519288d 100644 --- a/src/SonarQube.Client/ISonarQubeService.cs +++ b/src/SonarQube.Client/ISonarQubeService.cs @@ -36,60 +36,6 @@ public interface ISonarQubeService void Disconnect(); - Task> GetRulesAsync( - bool isActive, - string qualityProfileKey, - CancellationToken token); - - /// - /// Returns all properties for the project with the specified projectKey. If a project with such - /// key does not exist, returns the default property values for the connected SonarQube server. - /// - Task> GetAllPropertiesAsync(string projectKey, CancellationToken token); - - /// - /// Wrapper for GET api/qualityprofiles/search - /// - /// - Task> GetAllQualityProfilesAsync( - string project, - string organizationKey, - CancellationToken token); - - /// - /// Returns the suppressed issues for the specified project/branch. - /// - /// The project identifier - /// - /// (optional) The Sonar branch for which issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned. - /// - /// - /// (optional) The ids of the issues to return. If empty, all issues will be returned. - Task> GetSuppressedRoslynIssuesAsync( - string projectKey, - string branch, - string[] issueKeys, - CancellationToken token); - - /// - /// Returns the issues in the specified server component with the same rule id - /// - /// The project identifier - /// - /// (optional) The Sonar branch for which issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned - /// - /// The component identifier. Project/Directory/File - /// The Rule identifier. Is used to limit the number of issues in the response - /// - Task> GetIssuesForComponentAsync( - string projectKey, - string branch, - string componentKey, - string ruleId, - CancellationToken token); - Task> GetNotificationEventsAsync( string projectKey, DateTimeOffset eventsSince, @@ -122,11 +68,6 @@ Task> SearchFilesByNameAsync( /// Task> GetProjectBranchesAsync(string projectKey, CancellationToken token); - /// - /// Returns the inclusions/exclusions - /// - Task GetServerExclusions(string projectKey, CancellationToken token); - /// /// Creates a new for the given /// diff --git a/src/SonarQube.Client/SonarQubeService.cs b/src/SonarQube.Client/SonarQubeService.cs index ae7acb4462..2c34ac2038 100644 --- a/src/SonarQube.Client/SonarQubeService.cs +++ b/src/SonarQube.Client/SonarQubeService.cs @@ -115,55 +115,6 @@ public void Disconnect() requestFactory = null; } - public async Task> GetAllPropertiesAsync(string projectKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, - token); - - public async Task> GetAllQualityProfilesAsync(string project, string organizationKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = project; - request.OrganizationKey = GetOrganizationKeyForWebApiCalls(organizationKey, logger); - }, - token); - - public async Task> GetSuppressedRoslynIssuesAsync( - string projectKey, - string branch, - string[] issueKeys, - CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.Branch = branch; - request.IssueKeys = issueKeys; - request.Languages = string.Join(",", languageProvider.RoslynLanguages.Select(x => x.ServerLanguageKey)); - request.Statuses = "RESOLVED"; // Resolved issues will be hidden in SLVS - }, - token); - - public async Task> GetIssuesForComponentAsync( - string projectKey, - string branch, - string componentKey, - string ruleId, - CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.Branch = branch; - request.ComponentKey = componentKey; - request.RuleId = ruleId; - }, - token); - public async Task> GetNotificationEventsAsync( string projectKey, DateTimeOffset eventsSince, @@ -191,15 +142,6 @@ await InvokeCheckedRequestAsync( token ); - public async Task> GetRulesAsync(bool isActive, string qualityProfileKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.IsActive = isActive; - request.QualityProfileKey = qualityProfileKey; - }, - token); - public Uri GetViewIssueUrl(string projectKey, string issueKey) { EnsureIsConnected(); @@ -220,13 +162,6 @@ await InvokeCheckedRequestAsync GetServerExclusions(string projectKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, token); - public async Task CreateSSEStreamReader(string projectKey, CancellationToken token) { var networkStream = await InvokeCheckedRequestAsync( From fc824438cbd8c23a96574dbd1609721a91904906 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 3 Sep 2025 14:23:11 +0200 Subject: [PATCH 21/38] SLVS-2510 Rename properties specific to SqvsRoslynPlugin (#6400) --- .../Http/HttpServerConfigurationProviderTest.cs | 4 ++-- .../Http/HttpServerConfigurationProvider.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs index 68511b82e4..3e9036defa 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpServerConfigurationProviderTest.cs @@ -104,8 +104,8 @@ public void SetNewConfiguration_GeneratesDifferentProperties() [TestMethod] public void MapToInferredProperties_ReturnsExpectedProperties() { - var portKey = "sonar.cs.internal.roslynAnalyzerServerPort"; - var tokenKey = "sonar.cs.internal.roslynAnalyzerServerToken"; + var portKey = "sonar.sqvsRoslynPlugin.internal.serverPort"; + var tokenKey = "sonar.sqvsRoslynPlugin.internal.serverToken"; var analysisProperties = testSubject.CurrentConfiguration.MapToInferredProperties(); diff --git a/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs b/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs index 862eaf281e..fe47241d16 100644 --- a/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Http/HttpServerConfigurationProvider.cs @@ -70,8 +70,8 @@ public IHttpServerConfiguration CurrentConfiguration private sealed class HttpServerConfiguration : IHttpServerConfiguration { private const int TokenByteLength = 32; - private const string PortAnalysisPropertyKey = "sonar.cs.internal.roslynAnalyzerServerPort"; - private const string TokenAnalysisPropertyKey = "sonar.cs.internal.roslynAnalyzerServerToken"; + private const string PortAnalysisPropertyKey = "sonar.sqvsRoslynPlugin.internal.serverPort"; + private const string TokenAnalysisPropertyKey = "sonar.sqvsRoslynPlugin.internal.serverToken"; public int Port { get; } = GetAvailablePort(); public SecureString Token { get; } = GenerateSecureToken(); From 2a902d7754963ef4665feb8e22b88ff546aded00 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 3 Sep 2025 14:49:47 +0200 Subject: [PATCH 22/38] SLVS-2481 Cache analyzer assembly contents (#6396) --- SonarQube.VisualStudio.sln.DotSettings | 1 + .../EmbeddedDotnetAnalyzersLocatorTests.cs | 61 ++---- .../EmbeddedDotnetAnalyzersLocator.cs | 17 +- .../SonarLintDaemonPackage.cs | 3 + ...figurationParametersCacheExtensionsTest.cs | 165 ++++++++++++++++ ...oslynAnalysisConfigurationProviderTests.cs | 179 ++++++++++++++++-- .../RoslynAnalyzerProviderTests.cs | 150 ++++++++++++--- .../RoslynAnalysisServiceTests.cs | 2 +- .../AnalysisConfigurationParametersCache.cs | 76 ++++++++ .../IEmbeddedRoslynAnalyzersLocator.cs | 5 +- .../IRoslynAnalysisConfigurationProvider.cs | 5 +- .../Configuration/IRoslynAnalyzerProvider.cs | 5 + .../RoslynAnalysisConfigurationProvider.cs | 41 +++- .../Configuration/RoslynAnalyzerProvider.cs | 44 ++++- .../RoslynAnalysisService.cs | 6 +- 15 files changed, 648 insertions(+), 112 deletions(-) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs diff --git a/SonarQube.VisualStudio.sln.DotSettings b/SonarQube.VisualStudio.sln.DotSettings index 1c12bf3bca..5309ade584 100644 --- a/SonarQube.VisualStudio.sln.DotSettings +++ b/SonarQube.VisualStudio.sln.DotSettings @@ -25,6 +25,7 @@ False True CHOP_IF_LONG + True True True CHOP_IF_LONG diff --git a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs index 7684daf2cb..c49d8044aa 100644 --- a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs @@ -24,7 +24,6 @@ using SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; -using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.Integration.UnitTests.EmbeddedAnalyzers; @@ -41,19 +40,22 @@ public class EmbeddedDotnetAnalyzersLocatorTests private EmbeddedDotnetAnalyzersLocator testSubject; private IVsixRootLocator vsixRootLocator; + private ILanguageProvider languageProvider; [TestInitialize] public void TestInitialize() { vsixRootLocator = Substitute.For(); fileSystem = Substitute.For(); - testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, fileSystem); + languageProvider = Substitute.For(); + testSubject = new EmbeddedDotnetAnalyzersLocator(vsixRootLocator, languageProvider, fileSystem); } [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); [TestMethod] @@ -140,8 +142,9 @@ public void GetAnalyzerFullPaths_SearchesForFilesInsideVsix() } [TestMethod] - public void GetAnalyzerFullPathsByLanguage_BothEnterprise_GroupsEnterpriseDllsByLanguage() + public void GetAnalyzerFullPathsByLanguage_ReturnsExpectedPaths() { + languageProvider.RoslynLanguages.Returns([Language.CSharp, Language.VBNET]); fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ CSharpRegularAnalyzer, VbRegularAnalyzer, @@ -149,50 +152,14 @@ public void GetAnalyzerFullPathsByLanguage_BothEnterprise_GroupsEnterpriseDllsBy VbEnterpriseAnalyzer ]); - testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(true, true)).Should().BeEquivalentTo( - new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], [Language.VBNET] = [VbRegularAnalyzer, VbEnterpriseAnalyzer] }); - } - - [TestMethod] - public void GetAnalyzerFullPathsByLanguage_BothBasic_GroupsBasicDllsByLanguage() - { - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ - CSharpRegularAnalyzer, - VbRegularAnalyzer, - CSharpEnterpriseAnalyzer, - VbEnterpriseAnalyzer - ]); - - testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(false, false)).Should().BeEquivalentTo( - new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [VbRegularAnalyzer] }); - } - - [TestMethod] - public void GetAnalyzerFullPathsByLanguage_OnlyCsharpEnterprise_GroupsDllsByLanguage() - { - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ - CSharpRegularAnalyzer, - VbRegularAnalyzer, - CSharpEnterpriseAnalyzer, - VbEnterpriseAnalyzer - ]); - - testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(true, false)).Should().BeEquivalentTo( - new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], [Language.VBNET] = [VbRegularAnalyzer] }); - } - - [TestMethod] - public void GetAnalyzerFullPathsByLanguage_OnlyVbEnterprise_GroupsDllsByLanguage() - { - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns([ - CSharpRegularAnalyzer, - VbRegularAnalyzer, - CSharpEnterpriseAnalyzer, - VbEnterpriseAnalyzer - ]); - - testSubject.GetAnalyzerFullPathsByLanguage(new AnalyzerInfoDto(false, true)).Should().BeEquivalentTo( - new Dictionary> { [Language.CSharp] = [CSharpRegularAnalyzer], [Language.VBNET] = [VbRegularAnalyzer, VbEnterpriseAnalyzer] }); + testSubject.GetAnalyzerFullPathsByLicensedLanguage().Should().BeEquivalentTo( + new Dictionary> + { + [new LicensedRoslynLanguage(Language.CSharp, false)] = [CSharpRegularAnalyzer], + [new LicensedRoslynLanguage(Language.CSharp, true)] = [CSharpRegularAnalyzer, CSharpEnterpriseAnalyzer], + [new LicensedRoslynLanguage(Language.VBNET, false)] = [VbRegularAnalyzer], + [new LicensedRoslynLanguage(Language.VBNET, true)] = [VbRegularAnalyzer, VbEnterpriseAnalyzer], + }); } private static string GetAnalyzerFullPath(string pathInsideVsix, string analyzerFile) => Path.Combine(pathInsideVsix, analyzerFile); diff --git a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs index a20e161779..46c8e4f05d 100644 --- a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs +++ b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs @@ -26,7 +26,6 @@ using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; -using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; @@ -34,7 +33,7 @@ namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; [Export(typeof(IObsoleteDotnetAnalyzersLocator))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, IFileSystemService fileSystem) +internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, ILanguageProvider languageProvider, IFileSystemService fileSystem) : IEmbeddedDotnetAnalyzersLocator, IObsoleteDotnetAnalyzersLocator { private const string PathInsideVsix = "EmbeddedDotnetAnalyzerDLLs"; @@ -45,14 +44,18 @@ internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, public List GetBasicAnalyzerFullPaths() => GetBasicAnalyzerDlls().ToList(); - public Dictionary> GetAnalyzerFullPathsByLanguage(AnalyzerInfoDto analyzerInfoDto) + public Dictionary> GetAnalyzerFullPathsByLicensedLanguage() { + var languageToDllsMap = new Dictionary>(); var allAnalyzers = GetAllAnalyzerDlls(); - var languageToDllsMap = new Dictionary> + foreach (var roslynLanguage in languageProvider.RoslynLanguages) { - { RoslynLanguage.CSharp, GetAnalyzerFullPathsByLanguage(RoslynLanguage.CSharp, analyzerInfoDto.ShouldUseCsharpEnterprise, allAnalyzers) }, - { RoslynLanguage.VBNET, GetAnalyzerFullPathsByLanguage(RoslynLanguage.VBNET, analyzerInfoDto.ShouldUseVbEnterprise, allAnalyzers) } - }; + var basicKey = new LicensedRoslynLanguage(roslynLanguage, IsEnterprise: false); + languageToDllsMap.Add(basicKey, GetAnalyzerFullPathsByLanguage(basicKey.RoslynLanguage, basicKey.IsEnterprise, allAnalyzers)); + + var enterpriseKey = new LicensedRoslynLanguage(roslynLanguage, IsEnterprise: true); + languageToDllsMap.Add(enterpriseKey, GetAnalyzerFullPathsByLanguage(enterpriseKey.RoslynLanguage, enterpriseKey.IsEnterprise, allAnalyzers)); + } return languageToDllsMap; } diff --git a/src/Integration.Vsix/SonarLintDaemonPackage.cs b/src/Integration.Vsix/SonarLintDaemonPackage.cs index d2ab9af49a..9950f7436e 100644 --- a/src/Integration.Vsix/SonarLintDaemonPackage.cs +++ b/src/Integration.Vsix/SonarLintDaemonPackage.cs @@ -32,6 +32,7 @@ using SonarLint.VisualStudio.Integration.Vsix.Events; using SonarLint.VisualStudio.Integration.Vsix.Resources; using SonarLint.VisualStudio.RoslynAnalyzerServer; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.SLCore; using SonarLint.VisualStudio.SLCore.Analysis; using ErrorHandler = SonarLint.VisualStudio.Core.ErrorHandler; @@ -123,7 +124,9 @@ private async Task InitAsync() importBeforeFileGenerator.UpdateOrCreateTargetsFileAsync().Forget(); var thread = await this.GetMefServiceAsync(); + var roslynAnalyzerAssemblyLoader = await this.GetMefServiceAsync(); roslynAnalysisHttpServer = await this.GetMefServiceAsync(); + thread.RunOnBackgroundThread(() => roslynAnalyzerAssemblyLoader.LoadRoslynAnalyzerAssemblyContentsIfNeeded()).Forget(); thread.RunOnBackgroundThread(() => StartRoslynAnalysisHttpServerAsync().ConfigureAwait(false)).Forget(); slCoreHandler = await this.GetMefServiceAsync(); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs new file mode 100644 index 0000000000..4ce5cac31b --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/AnalysisConfigurationParametersCacheExtensionsTest.cs @@ -0,0 +1,165 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configuration; + +[TestClass] +public class AnalysisConfigurationParametersCacheExtensionsTest +{ + private readonly AnalyzerInfoDto defaultAnalyzerInfoDto = new(false, false); + private readonly KeyValuePair disableRazorAnalysisProp = new("sonar.cs.internal.disableRazor", "true"); + private readonly ActiveRuleDto s101RuleWithParam = new("S101", new Dictionary { { "threshold", "3" } }); + + [TestMethod] + public void ShouldInvalidateCache_CacheIsNull_ReturnsTrue() + { + AnalysisConfigurationParametersCache? testSubject = null; + + var result = testSubject.ShouldInvalidateCache([], [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void ShouldInvalidateCache_AnalyzerInfoDtoHasDifferentValues_ReturnsTrue(bool useSharpEnterprise, bool useVbNetEnterprise) + { + var testSubject = new AnalysisConfigurationParametersCache([], [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], [], new(useSharpEnterprise, useVbNetEnterprise)); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + [DataRow(false, false)] + public void ShouldInvalidateCache_AnalyzerInfoDtoHasSameValues_ReturnsFalse(bool useSharpEnterprise, bool useVbNetEnterprise) + { + var testSubject = new AnalysisConfigurationParametersCache([], [], new(useSharpEnterprise, useVbNetEnterprise)); + + var result = testSubject.ShouldInvalidateCache([], [], new(useSharpEnterprise, useVbNetEnterprise)); + + result.Should().BeFalse(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameActiveRules_ReturnsFalse() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var sameActiveRules = new List { s101RuleWithParam with { Parameters = new Dictionary(s101RuleWithParam.Parameters) } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(sameActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeFalse(); + } + + [TestMethod] + public void ShouldInvalidateCache_DifferentActiveRules_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { new("S102", new Dictionary { { "timeout", "60" } }) }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameRuleWithDifferentParameter_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { s101RuleWithParam with { Parameters = new Dictionary { { "timeout", "60" } } } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameRuleWithDifferentParameters_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { s101RuleWithParam with { Parameters = [] } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameRuleWithDifferentParameterValue_ReturnsTrue() + { + var activeRules = new Dictionary { { s101RuleWithParam.RuleId, s101RuleWithParam } }; + var newActiveRules = new List { s101RuleWithParam with { Parameters = new Dictionary { { "threshold", "5" } } } }; + var testSubject = new AnalysisConfigurationParametersCache(activeRules, [], defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache(newActiveRules, [], defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameAnalysisProperties_ReturnsFalse() + { + var analysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var sameAnalysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var testSubject = new AnalysisConfigurationParametersCache([], analysisProperties, defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], sameAnalysisProperties, defaultAnalyzerInfoDto); + + result.Should().BeFalse(); + } + + [TestMethod] + public void ShouldInvalidateCache_SameAnalysisPropertiesWithDifferentValue_ReturnsTrue() + { + var analysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var newAnalysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, "false" } }; + var testSubject = new AnalysisConfigurationParametersCache([], analysisProperties, defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], newAnalysisProperties, defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } + + [TestMethod] + public void ShouldInvalidateCache_DifferentAnalysisProperties_ReturnsTrue() + { + var analysisProperties = new Dictionary { { disableRazorAnalysisProp.Key, disableRazorAnalysisProp.Value } }; + var newAnalysisProperties = new Dictionary(); + var testSubject = new AnalysisConfigurationParametersCache([], analysisProperties, defaultAnalyzerInfoDto); + + var result = testSubject.ShouldInvalidateCache([], newAnalysisProperties, defaultAnalyzerInfoDto); + + result.Should().BeTrue(); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs index c0bfd88146..1308191f6f 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -22,6 +22,9 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.TestInfrastructure; @@ -33,10 +36,13 @@ public class RoslynAnalysisConfigurationProviderTests { private static readonly ImmutableDictionary DefaultAnalyzers = new Dictionary { { Language.CSharp, new AnalyzerAssemblyContents() } }.ToImmutableDictionary(); - private static readonly List DefaultActiveRules = new(); - private static readonly Dictionary DefaultAnalysisProperties = new(); + private static readonly List DefaultActiveRules = []; + private static readonly Dictionary DefaultAnalysisProperties = []; private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); + private static readonly Dictionary DefaultRoslynAnalysisProfile = []; + private IThreadHandling threadHandling = null!; + private IAsyncLock asyncLock = null!; private ISonarLintXmlProvider sonarLintXmlProvider = null!; private IRoslynAnalyzerProvider roslynAnalyzerProvider = null!; private IRoslynAnalysisProfilesProvider analyzerProfilesProvider = null!; @@ -46,17 +52,26 @@ private static readonly ImmutableDictionary(); + threadHandling = Substitute.ForPartsOf(); sonarLintXmlProvider = Substitute.For(); roslynAnalyzerProvider = Substitute.For(); roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto).Returns(DefaultAnalyzers); + asyncLock = Substitute.For(); + asyncLockFactory.Create().Returns(asyncLock); analyzerProfilesProvider = Substitute.For(); + analyzerProfilesProvider + .GetAnalysisProfilesByLanguage(Arg.Any>(), Arg.Any>(), Arg.Any>()) + .Returns(DefaultRoslynAnalysisProfile); testLogger = Substitute.ForPartsOf(); testSubject = new RoslynAnalysisConfigurationProvider( sonarLintXmlProvider, roslynAnalyzerProvider, analyzerProfilesProvider, + asyncLockFactory, + threadHandling, testLogger); } @@ -66,6 +81,8 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); [TestMethod] @@ -78,7 +95,7 @@ public void Ctor_SetsLogContext() => Resources.RoslynAnalysisConfigurationLogContext); [TestMethod] - public void GetConfiguration_CreatesConfigurationForEachLanguage() + public async Task GetConfigurationAsync_CreatesConfigurationForEachLanguage() { var roslynAnalysisProfiles = new Dictionary { @@ -102,7 +119,7 @@ public void GetConfiguration_CreatesConfigurationForEachLanguage() analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(roslynAnalysisProfiles); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Keys.Should().BeEquivalentTo(roslynAnalysisProfiles.Keys); foreach (var language in roslynAnalysisProfiles.Keys) @@ -116,7 +133,7 @@ public void GetConfiguration_CreatesConfigurationForEachLanguage() } [TestMethod] - public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() + public async Task GetConfigurationAsync_NoAnalyzers_LogsAndExcludesLanguage() { var language = Language.CSharp; var roslynAnalysisProfiles = new Dictionary @@ -126,21 +143,21 @@ public void GetConfiguration_NoAnalyzers_LogsAndExcludesLanguage() ImmutableArray.Empty, CreateTestCodeFixProviders(), [CreateRuleConfiguration(language, "S001")], - new Dictionary()) + []) } }; analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(roslynAnalysisProfiles); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Should().BeEmpty(); testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoAnalyzers, language.Name)); } [TestMethod] - public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() + public async Task GetConfigurationAsync_NoActiveRules_LogsAndExcludesLanguage() { var language = Language.CSharp; var roslynAnalysisProfiles = new Dictionary @@ -150,30 +167,155 @@ public void GetConfiguration_NoActiveRules_LogsAndExcludesLanguage() CreateTestAnalyzers(1), CreateTestCodeFixProviders(), [CreateRuleConfiguration(language, "S001", false), CreateRuleConfiguration(language, "S002", false)], - new Dictionary()) + []) } }; analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) .Returns(roslynAnalysisProfiles); - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Should().BeEmpty(); testLogger.AssertPartialOutputStringExists(string.Format(Resources.RoslynAnalysisConfigurationNoActiveRules, language.Name)); } [TestMethod] - public void GetConfiguration_NoAnalysisProfiles_ReturnsEmptyDictionary() + public async Task GetConfigurationAsync_NoAnalysisProfiles_ReturnsEmptyDictionary() { - analyzerProfilesProvider.GetAnalysisProfilesByLanguage(DefaultAnalyzers, DefaultActiveRules, DefaultAnalysisProperties) - .Returns(new Dictionary()); - - var result = testSubject.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + var result = await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); result.Should().BeEmpty(); } + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameActiveRules_Caches() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var sameActiveRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(sameActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(sameActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_DifferentActiveRules_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S102", new Dictionary { { "threshold", "3" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameRuleWithDifferentParameter_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S101", new Dictionary { { "timeout", "60" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameRuleWithDifferentParameters_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S101", []) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameRuleWithDifferentParameterValue_InvalidatesCache() + { + var activeRules = new List { new("S101", new Dictionary { { "threshold", "3" } }) }; + var newActiveRules = new List { new("S101", new Dictionary { { "threshold", "5" } }) }; + + await testSubject.GetConfigurationAsync(activeRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(newActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), activeRules, DefaultAnalysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), newActiveRules, DefaultAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameAnalysisProperties_Caches() + { + var analysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + var sameAnalysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + + await testSubject.GetConfigurationAsync(DefaultActiveRules, analysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, sameAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, sameAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, analysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_SameAnalysisPropertyWithDifferentValue_InvalidatesCache() + { + var analysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + var newAnalysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "false" } }; + + await testSubject.GetConfigurationAsync(DefaultActiveRules, analysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, analysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, newAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_DifferentAnalysisProperties_InvalidatesCache() + { + var analysisProperties = new Dictionary { { "sonar.cs.internal.disableRazor", "true" } }; + var newAnalysisProperties = new Dictionary(); + + await testSubject.GetConfigurationAsync(DefaultActiveRules, analysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, newAnalysisProperties, DefaultAnalyzerInfoDto); + + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, analysisProperties); + analyzerProfilesProvider.Received(1).GetAnalysisProfilesByLanguage(Arg.Any>(), DefaultActiveRules, newAnalysisProperties); + } + + [TestMethod] + public async Task GetConfigurationAsync_MultipleCalls_Locks() + { + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + asyncLock.Received(3).AcquireAsync().IgnoreAwaitForAssert(); + } + + [TestMethod] + public async Task GetConfigurationAsync_RunsOnBackgroundThread() + { + await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); + + threadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>>()).IgnoreAwaitForAssert(); + } + private Dictionary SetUpXmlConfigurations(Dictionary profiles) { var xmlConfigurations = new Dictionary(); @@ -185,7 +327,7 @@ private Dictionary SetUpXmlConfig return xmlConfigurations; } - private RoslynRuleConfiguration CreateRuleConfiguration( + private static RoslynRuleConfiguration CreateRuleConfiguration( Language language, string ruleKey, bool isActive = true) => @@ -193,9 +335,10 @@ private RoslynRuleConfiguration CreateRuleConfiguration( isActive, []); - private ImmutableArray CreateTestAnalyzers(int count) => Enumerable.Range(0, count).Select(_ => Substitute.For()).ToImmutableArray(); + private static ImmutableArray CreateTestAnalyzers(int count) => Enumerable.Range(0, count).Select(_ => Substitute.For()).ToImmutableArray(); - private ImmutableDictionary> CreateTestCodeFixProviders() => ImmutableDictionary>.Empty.Add("any", [Substitute.For()]); + private static ImmutableDictionary> CreateTestCodeFixProviders() => + ImmutableDictionary>.Empty.Add("any", [Substitute.For()]); private SonarLintXmlConfigurationFile SetUpXmlProvider(RoslynAnalysisProfile profile) { diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs index fdf4e4565c..27733ed2a4 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerProviderTests.cs @@ -33,7 +33,13 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Configu public class RoslynAnalyzerProviderTests { private const string CsharpAnalyzerPath = "c:\\analyzers\\csharp.dll"; + private const string CsharpEnterpriseAnalyzerPath = "c:\\analyzers\\csharp.enterprise.dll"; private const string VbAnalyzerPath = "c:\\analyzers\\vb.dll"; + private const string VbEnterpriseAnalyzerPath = "c:\\analyzers\\vb.enterprise.dll"; + private readonly DiagnosticAnalyzer CsharpAnalyzer = CreateAnalyzerWithDiagnostic(); + private readonly DiagnosticAnalyzer CsharpEnterpriseAnalyzer = CreateAnalyzerWithDiagnostic(); + private readonly DiagnosticAnalyzer VbAnalyzer = CreateAnalyzerWithDiagnostic(); + private readonly DiagnosticAnalyzer VbEnterpriseAnalyzer = CreateAnalyzerWithDiagnostic(); private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); private IEmbeddedDotnetAnalyzersLocator analyzersLocator = null!; @@ -49,18 +55,24 @@ public void TestInitialize() } [TestMethod] - public void MefCtor_CheckIsExported() => + public void MefCtor_IRoslynAnalyzerProvider_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); + [TestMethod] + public void MefCtor_IRoslynAnalyzerAssemblyContentsLoader_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() + public void LoadAndProcessAnalyzerAssemblies_NoAnalyzers_ReturnsEmptyDictionary() { - analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()).Returns(new Dictionary>()); + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage().Returns(new Dictionary>()); var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); @@ -68,10 +80,10 @@ public void GetAnalyzersByLanguage_NoAnalyzers_ReturnsEmptyDictionary() } [TestMethod] - public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrectDictionary() + public void LoadAndProcessAnalyzerAssemblies_WithAnalyzers_LoadsAnalyzersAndReturnsCorrectDictionary() { - analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()) - .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath] }, { new(Language.VBNET, false), [VbAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("CS0001"); var vbAnalyzer = CreateAnalyzerWithDiagnostic("VB0001"); var csharpCodeFixer = CreateCodeFixProviderWithDiagnostics("CS0001"); @@ -94,10 +106,10 @@ public void GetAnalyzersByLanguage_WithAnalyzers_LoadsAnalyzersAndReturnsCorrect } [TestMethod] - public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() + public void LoadAndProcessAnalyzerAssemblies_IgnoresDuplicateIdsForTheSameLanguage() { - analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()) - .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] }, { Language.VBNET, [VbAnalyzerPath] } }); + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath] }, { new(Language.VBNET, false), [VbAnalyzerPath] } }); var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001", "SDUPLICATE"); var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "SDUPLICATE"); var vbAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002"); @@ -112,11 +124,11 @@ public void GetAnalyzersByLanguage_IgnoresDuplicateIdsForTheSameLanguage() } [TestMethod] - public void GetAnalyzersByLanguage_MultipleAnalyzersPerLanguage_CombinesAllRules() + public void LoadAndProcessAnalyzerAssemblies_MultipleAnalyzersPerLanguage_CombinesAllRules() { const string csharpAnalyzerPath2 = "c:\\analyzers\\csharp2.dll"; - analyzersLocator.GetAnalyzerFullPathsByLanguage(Arg.Any()) - .Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath, csharpAnalyzerPath2] } }); + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath, csharpAnalyzerPath2] } }); var csharpAnalyzer1 = CreateAnalyzerWithDiagnostic("S001"); var csharpAnalyzer2 = CreateAnalyzerWithDiagnostic("S002", "S003"); var csharpAnalyzer3 = CreateAnalyzerWithDiagnostic("S004"); @@ -131,11 +143,10 @@ public void GetAnalyzersByLanguage_MultipleAnalyzersPerLanguage_CombinesAllRules } [TestMethod] - public void GetAnalyzersByLanguage_NoCodeFixProviders_ReturnsEmptyMap() + public void LoadAndProcessAnalyzerAssemblies_NoCodeFixProviders_ReturnsEmptyMap() { - analyzersLocator.GetAnalyzerFullPathsByLanguage(DefaultAnalyzerInfoDto).Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); - roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [])); + MockCodeProvidersForCsharp(csharpAnalyzer, []); var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); @@ -143,13 +154,11 @@ public void GetAnalyzersByLanguage_NoCodeFixProviders_ReturnsEmptyMap() } [TestMethod] - public void GetAnalyzersByLanguage_CodeFixProviderWithMultipleDiagnostics_AddedToAllMappings() + public void LoadAndProcessAnalyzerAssemblies_CodeFixProviderWithMultipleDiagnostics_AddedToAllMappings() { - analyzersLocator.GetAnalyzerFullPathsByLanguage(DefaultAnalyzerInfoDto).Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001", "S002", "S003"); var codeFixProvider = CreateCodeFixProviderWithDiagnostics("S001", "S002"); - - roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [codeFixProvider])); + MockCodeProvidersForCsharp(csharpAnalyzer, codeFixProvider); var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); @@ -158,14 +167,12 @@ public void GetAnalyzersByLanguage_CodeFixProviderWithMultipleDiagnostics_AddedT } [TestMethod] - public void GetAnalyzersByLanguage_MultipleCodeFixProvidersForSameId_AllAddedToSameCollection() + public void LoadAndProcessAnalyzerAssemblies_MultipleCodeFixProvidersForSameId_AllAddedToSameCollection() { - analyzersLocator.GetAnalyzerFullPathsByLanguage(DefaultAnalyzerInfoDto).Returns(new Dictionary> { { Language.CSharp, [CsharpAnalyzerPath] } }); var csharpAnalyzer = CreateAnalyzerWithDiagnostic("S001"); var codeFixProvider1 = CreateCodeFixProviderWithDiagnostics("S001"); var codeFixProvider2 = CreateCodeFixProviderWithDiagnostics("S001"); - - roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], [codeFixProvider1, codeFixProvider2])); + MockCodeProvidersForCsharp(csharpAnalyzer, codeFixProvider1, codeFixProvider2); var result = testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); @@ -173,6 +180,80 @@ public void GetAnalyzersByLanguage_MultipleCodeFixProvidersForSameId_AllAddedToS new Dictionary> { { "S001", [codeFixProvider1, codeFixProvider2] } }); } + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_BothEnterprise_ReturnsEnterpriseDlls() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: true, ShouldUseVbEnterprise: true); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpEnterpriseAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbEnterpriseAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_BothBasic_ReturnsBasicDlls() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: false, ShouldUseVbEnterprise: false); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_OnlyCsharpEnterprise_ReturnsCsharpEnterpriseAndVbBasic() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: true, ShouldUseVbEnterprise: false); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpEnterpriseAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_OnlyVbEnterprise_ReturnsVbEnterpriseAndCsharpBasic() + { + MockAnalyzerFullPathsByLicensedLanguage(); + var analyzerInfo = new AnalyzerInfoDto(ShouldUseCsharpEnterprise: false, ShouldUseVbEnterprise: true); + + var result = testSubject.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + + result[Language.CSharp].Analyzers.Should().BeEquivalentTo(new List { CsharpAnalyzer }); + result[Language.VBNET].Analyzers.Should().BeEquivalentTo(new List { VbEnterpriseAnalyzer }); + } + + [TestMethod] + public void LoadAndProcessAnalyzerAssemblies_MultipleCalls_AnalyzerAssemblyContentsAreCached() + { + MockCodeProvidersForCsharp(CsharpAnalyzer); + + testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + testSubject.LoadAndProcessAnalyzerAssemblies(DefaultAnalyzerInfoDto); + + analyzersLocator.Received(1).GetAnalyzerFullPathsByLicensedLanguage(); + roslynAnalyzerLoader.Received(1).LoadAnalyzerAssembly(Arg.Any()); + } + + [TestMethod] + public void LoadRoslynAnalyzerAssemblyContentsIfNeeded_MultipleCalls_AnalyzerAssemblyContentsAreCached() + { + MockCodeProvidersForCsharp(CsharpAnalyzer); + + testSubject.LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + testSubject.LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + testSubject.LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + + analyzersLocator.Received(1).GetAnalyzerFullPathsByLicensedLanguage(); + roslynAnalyzerLoader.Received(1).LoadAnalyzerAssembly(Arg.Any()); + } + private static DiagnosticAnalyzer CreateAnalyzerWithDiagnostic(params string[] diagnosticIds) { var analyzer = Substitute.For(); @@ -195,4 +276,27 @@ private static DiagnosticDescriptor CreateDiagnosticDescriptor(string id) => "any category", DiagnosticSeverity.Warning, true); + + private void MockCodeProvidersForCsharp(DiagnosticAnalyzer csharpAnalyzer, params CodeFixProvider[] codeFixProviders) + { + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> { { new(Language.CSharp, false), [CsharpAnalyzerPath] } }); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([csharpAnalyzer], codeFixProviders)); + } + + private void MockAnalyzerFullPathsByLicensedLanguage() + { + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpAnalyzerPath).Returns(new LoadedAnalyzerClasses([CsharpAnalyzer], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(CsharpEnterpriseAnalyzerPath).Returns(new LoadedAnalyzerClasses([CsharpEnterpriseAnalyzer], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbAnalyzerPath).Returns(new LoadedAnalyzerClasses([VbAnalyzer], [])); + roslynAnalyzerLoader.LoadAnalyzerAssembly(VbEnterpriseAnalyzerPath).Returns(new LoadedAnalyzerClasses([VbEnterpriseAnalyzer], [])); + analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage() + .Returns(new Dictionary> + { + { new(Language.CSharp, false), [CsharpAnalyzerPath] }, + { new(Language.CSharp, true), [CsharpEnterpriseAnalyzerPath] }, + { new(Language.VBNET, false), [VbAnalyzerPath] }, + { new(Language.VBNET, true), [VbEnterpriseAnalyzerPath] }, + }); + } } diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs index f930b72196..04442a3907 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -67,7 +67,7 @@ public void MefCtor_CheckIsExported() => public async Task AnalyzeAsync_PassesCorrectArgumentsToEngine() { string[] filePaths = [@"C:\file1.cs", @"C:\folder\file2.cs"]; - analysisConfigurationProvider.GetConfiguration(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto).Returns(DefaultAnalysisConfigurations); + analysisConfigurationProvider.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto).Returns(DefaultAnalysisConfigurations); analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(Arg.Is(x => x.SequenceEqual(filePaths))).Returns(DefaultProjectAnalysisRequests); analysisEngine.AnalyzeAsync(DefaultProjectAnalysisRequests, DefaultAnalysisConfigurations, Arg.Any()).Returns(DefaultIssues); var analysisRequest = new AnalysisRequest diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs new file mode 100644 index 0000000000..0f3559be0b --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/AnalysisConfigurationParametersCache.cs @@ -0,0 +1,76 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; + +internal sealed record AnalysisConfigurationParametersCache( + Dictionary ActiveRuleDtos, + Dictionary AnalysisProperties, + AnalyzerInfoDto AnalyzerInfo); + +internal static class AnalysisConfigurationParametersCacheExtensions +{ + public static bool ShouldInvalidateCache( + this AnalysisConfigurationParametersCache? cache, + List newActiveRuleDtos, + Dictionary newAnalysisProperties, + AnalyzerInfoDto analyzerInfo) => + cache == null + || cache.AnalyzerInfo != analyzerInfo + || !AreSameActiveRuleDtos(newActiveRuleDtos, cache.ActiveRuleDtos) + || !AreDictionariesEqual(newAnalysisProperties, cache.AnalysisProperties); + + private static bool AreSameActiveRuleDtos(List newActiveRuleDtos, Dictionary oldActiveRuleDtos) + { + if (oldActiveRuleDtos.Count != newActiveRuleDtos.Count) + { + return false; + } + + foreach (var newRule in newActiveRuleDtos) + { + if (!oldActiveRuleDtos.TryGetValue(newRule.RuleId, out var cachedActiveRuleDto) || + !AreDictionariesEqual(newRule.Parameters, cachedActiveRuleDto.Parameters)) + { + return false; + } + } + return true; + } + + private static bool AreDictionariesEqual(Dictionary newDictionary, Dictionary oldDictionary) + { + if (newDictionary.Count != oldDictionary.Count) + { + return false; + } + + foreach (var newKvp in newDictionary) + { + if (!oldDictionary.TryGetValue(newKvp.Key, out var oldValue) || oldValue != newKvp.Value) + { + return false; + } + } + return true; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs index 675a41229d..33f5221bf1 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IEmbeddedRoslynAnalyzersLocator.cs @@ -19,11 +19,12 @@ */ using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; public interface IEmbeddedDotnetAnalyzersLocator { - Dictionary> GetAnalyzerFullPathsByLanguage(AnalyzerInfoDto analyzerInfoDto); + Dictionary> GetAnalyzerFullPathsByLicensedLanguage(); } + +public record LicensedRoslynLanguage(RoslynLanguage RoslynLanguage, bool IsEnterprise); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs index 7959b87772..701c17d2fc 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs @@ -25,5 +25,8 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalysisConfigurationProvider { - IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties, AnalyzerInfoDto analyzerInfo); + Task> GetConfigurationAsync( + List activeRules, + Dictionary analysisProperties, + AnalyzerInfoDto analyzerInfo); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs index 687ec50159..6c9d47dc9d 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalyzerProvider.cs @@ -31,6 +31,11 @@ internal interface IRoslynAnalyzerProvider ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo); } +public interface IRoslynAnalyzerAssemblyContentsLoader +{ + void LoadRoslynAnalyzerAssemblyContentsIfNeeded(); +} + internal readonly record struct AnalyzerAssemblyContents( ImmutableArray Analyzers, ImmutableHashSet SupportedRuleKeys, diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs index 442cf42c02..407305e377 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -21,6 +21,7 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; @@ -32,16 +33,45 @@ internal class RoslynAnalysisConfigurationProvider( ISonarLintXmlProvider sonarLintXmlProvider, IRoslynAnalyzerProvider roslynAnalyzerProvider, IRoslynAnalysisProfilesProvider analyzerProfilesProvider, + IAsyncLockFactory asyncLockFactory, + IThreadHandling threadHandling, ILogger logger) : IRoslynAnalysisConfigurationProvider { + private readonly IAsyncLock asyncLock = asyncLockFactory.Create(); private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + private AnalysisConfigurationCache? cache; - public IReadOnlyDictionary GetConfiguration(List activeRules, Dictionary? analysisProperties, AnalyzerInfoDto analyzerInfo) - { - // todo add caching https://sonarsource.atlassian.net/browse/SLVS-2481 + public Task> GetConfigurationAsync( + List activeRules, + Dictionary analysisProperties, + AnalyzerInfoDto analyzerInfo) => + threadHandling.RunOnBackgroundThread(async () => + { + using (await asyncLock.AcquireAsync()) + { + if (!cache.HasValue || cache.Value.Parameters.ShouldInvalidateCache(activeRules, analysisProperties, analyzerInfo)) + { + BuildConfigurations(activeRules, analysisProperties, analyzerInfo); + } + return cache!.Value.Configurations; + } + }); - var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(analyzerInfo), activeRules, analysisProperties); + private void BuildConfigurations( + List activeRules, + Dictionary analysisProperties, + AnalyzerInfoDto analyzerInfo) + { + var analyzerAssemblyContents = roslynAnalyzerProvider.LoadAndProcessAnalyzerAssemblies(analyzerInfo); + var analysisProfilesByLanguage = analyzerProfilesProvider.GetAnalysisProfilesByLanguage(analyzerAssemblyContents, activeRules, analysisProperties); + var activeRulesMap = activeRules.ToDictionary(r => r.RuleId, r => r); + cache = new AnalysisConfigurationCache( + new AnalysisConfigurationParametersCache(activeRulesMap, analysisProperties, analyzerInfo), + BuildConfigurations(analysisProfilesByLanguage)); + } + private IReadOnlyDictionary BuildConfigurations(Dictionary analysisProfilesByLanguage) + { var configurations = new Dictionary(); foreach (var analyzerAndLanguage in analysisProfilesByLanguage) { @@ -70,7 +100,8 @@ public IReadOnlyDictionary GetConfigurati analysisProfile.Analyzers, analysisProfile.CodeFixProvidersByRuleKey)); } - return configurations; } + + private record struct AnalysisConfigurationCache(AnalysisConfigurationParametersCache Parameters, IReadOnlyDictionary Configurations); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs index 38d6333ea1..068a93301c 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerProvider.cs @@ -28,16 +28,50 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [Export(typeof(IRoslynAnalyzerProvider))] +[Export(typeof(IRoslynAnalyzerAssemblyContentsLoader))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class RoslynAnalyzerProvider(IEmbeddedDotnetAnalyzersLocator analyzersLocator, IRoslynAnalyzerLoader roslynAnalyzerLoader) : IRoslynAnalyzerProvider +internal class RoslynAnalyzerProvider(IEmbeddedDotnetAnalyzersLocator analyzersLocator, IRoslynAnalyzerLoader roslynAnalyzerLoader) : IRoslynAnalyzerProvider, IRoslynAnalyzerAssemblyContentsLoader { - public ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo) => - LoadFromAssemblies(analyzersLocator.GetAnalyzerFullPathsByLanguage(analyzerInfo)); + private ImmutableDictionary? cachedAnalyzerAssemblyContents; + private static readonly object LockObj = new(); - private ImmutableDictionary LoadFromAssemblies(Dictionary> analyzerFullPathsByLanguage) + public ImmutableDictionary LoadAndProcessAnalyzerAssemblies(AnalyzerInfoDto analyzerInfo) { - var builder = ImmutableDictionary.CreateBuilder(); + LoadRoslynAnalyzerAssemblyContentsIfNeeded(); + + return cachedAnalyzerAssemblyContents! + .Where(kvp => FilterByLicense(kvp, analyzerInfo)) + .ToDictionary(kvp => kvp.Key.RoslynLanguage, kvp => kvp.Value) + .ToImmutableDictionary(); + } + + public void LoadRoslynAnalyzerAssemblyContentsIfNeeded() + { + lock (LockObj) + { + if (cachedAnalyzerAssemblyContents != null) + { + return; + } + var analyzerFullPathsByLanguage = analyzersLocator.GetAnalyzerFullPathsByLicensedLanguage(); + cachedAnalyzerAssemblyContents = LoadFromAssemblies(analyzerFullPathsByLanguage); + } + } + + private static bool FilterByLicense(KeyValuePair kvp, AnalyzerInfoDto analyzerInfo) + { + if (kvp.Key.RoslynLanguage.Equals(Language.VBNET)) + { + return kvp.Key.IsEnterprise == analyzerInfo.ShouldUseVbEnterprise; + } + + return kvp.Key.IsEnterprise == analyzerInfo.ShouldUseCsharpEnterprise; + } + + private ImmutableDictionary LoadFromAssemblies(Dictionary> analyzerFullPathsByLanguage) + { + var builder = ImmutableDictionary.CreateBuilder(); foreach (var languageAndAnalyzers in analyzerFullPathsByLanguage) { diff --git a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs index e3a4183a0a..24a568be94 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -33,11 +33,11 @@ internal class RoslynAnalysisService( IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService { - public Task> AnalyzeAsync( + public async Task> AnalyzeAsync( AnalysisRequest analysisRequest, CancellationToken cancellationToken) => - analysisEngine.AnalyzeAsync( + await analysisEngine.AnalyzeAsync( analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileNames.Select(x => x.LocalPath).ToArray()), - analysisConfigurationProvider.GetConfiguration(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), + await analysisConfigurationProvider.GetConfigurationAsync(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), cancellationToken); } From b13435073008976d88b3f26be367dcb013761631 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 5 Sep 2025 10:15:44 +0200 Subject: [PATCH 23/38] SLVS-2539 Fix binding no longer works due to configuration not being persisted (#6403) --- .../Binding/BindingProcessFactoryTests.cs | 67 -------- .../Binding/BindingProcessImplTests.cs | 152 ------------------ .../UnintrusiveBindingControllerTests.cs | 38 ++--- .../Migration/ConnectedModeMigrationTests.cs | 96 +++++------ .../Binding/BindingProcessImpl.cs | 62 ------- src/ConnectedMode/Binding/IBindingProcess.cs | 39 ----- .../Binding/IBindingProcessFactory.cs | 58 ------- .../Binding/IUnintrusiveBindingController.cs | 31 +--- .../Migration/ConnectedModeMigration.cs | 11 +- 9 files changed, 63 insertions(+), 491 deletions(-) delete mode 100644 src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs delete mode 100644 src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs delete mode 100644 src/ConnectedMode/Binding/BindingProcessImpl.cs delete mode 100644 src/ConnectedMode/Binding/IBindingProcess.cs delete mode 100644 src/ConnectedMode/Binding/IBindingProcessFactory.cs diff --git a/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs b/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs deleted file mode 100644 index 3a4e745ca2..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/BindingProcessFactoryTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding -{ - [TestClass] - public class BindingProcessFactoryTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Create_ReturnsProcessImpl() - { - var bindingArgs = new BindCommandArgs(new BoundServerProject("any", "any", new ServerConnection.SonarCloud("any"))); - - var testSubject = CreateTestSubject(); - - var actual = testSubject.Create(bindingArgs); - actual.Should().Should().NotBeNull(); - actual.Should().BeOfType(); - } - - private static BindingProcessFactory CreateTestSubject( - IQualityProfileDownloader qualityProfileDownloader = null, - ILogger logger = null) - { - qualityProfileDownloader ??= Mock.Of(); - logger ??= new TestLogger(logToConsole: true); - - return new BindingProcessFactory(qualityProfileDownloader, logger); - } - - } -} diff --git a/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs b/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs deleted file mode 100644 index f82f1fd662..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/BindingProcessImplTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.Persistence; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Helpers; -using System.Security; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding -{ - [TestClass] - public class BindingProcessImplTests - { - #region Tests - - [TestMethod] - public void Ctor_ArgChecks() - { - var bindingArgs = CreateBindCommandArgs(); - var qpDownloader = Mock.Of(); - Mock.Of(); - var logger = Mock.Of(); - - // 1. Null binding args - Action act = () => new BindingProcessImpl(null, qpDownloader, logger); - act.Should().ThrowExactly().And.ParamName.Should().Be("bindingArgs"); - - // 2. Null QP downloader - act = () => new BindingProcessImpl(bindingArgs, null, logger); - act.Should().ThrowExactly().And.ParamName.Should().Be("qualityProfileDownloader"); - - // 3. Null logger - act = () => new BindingProcessImpl(bindingArgs, qpDownloader, null); - act.Should().ThrowExactly().And.ParamName.Should().Be("logger"); - } - - [TestMethod] - public async Task DownloadQualityProfile_CreatesBoundProjectAndCallsQPDownloader() - { - var qpDownloader = new Mock(); - var progress = Mock.Of>(); - - var bindingArgs = CreateBindCommandArgs("the project key", "http://theServer"); - - var testSubject = CreateTestSubject(bindingArgs, - qpDownloader: qpDownloader.Object); - - // Act - var result = await testSubject.DownloadQualityProfileAsync(progress, CancellationToken.None); - - result.Should().BeTrue(); - - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), progress, It.IsAny()), - Times.Once); - - var actualProject = (BoundServerProject)qpDownloader.Invocations[0].Arguments[0]; - - // Check the bound project was correctly constructed from the BindCommandArgs - actualProject.Should().NotBeNull(); - actualProject.ServerConnection.ServerUri.Should().Be("http://theServer"); - actualProject.ServerProjectKey.Should().Be("the project key"); - } - - [TestMethod] - public async Task DownloadQualityProfile_HandlesInvalidOperationException() - { - var qpDownloader = new Mock(); - qpDownloader - .Setup(x => - x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Throws(new InvalidOperationException()); - - var testSubject = CreateTestSubject( - qpDownloader: qpDownloader.Object); - - // Act - var result = - await testSubject.DownloadQualityProfileAsync(Mock.Of>(), CancellationToken.None); - - result.Should().BeFalse(); - qpDownloader.Verify(x => x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny()), - Times.Once); - } - - #endregion Tests - - #region Helpers - - private BindingProcessImpl CreateTestSubject(BindCommandArgs bindingArgs = null, - IQualityProfileDownloader qpDownloader = null, - ILogger logger = null) - { - bindingArgs = bindingArgs ?? CreateBindCommandArgs(); - qpDownloader ??= Mock.Of(); - logger ??= new TestLogger(logToConsole: true); - - return new BindingProcessImpl(bindingArgs, - qpDownloader, - logger); - } - - private BindCommandArgs CreateBindCommandArgs(string projectKey = "key", string serverUri = "http://any") - { - return new BindCommandArgs(new BoundServerProject("any", projectKey, new ServerConnection.SonarQube(new Uri(serverUri)))); - } - - private static void CheckIsExpectedPassword(string expectedRawPassword, SecureString actualPassword) - { - // The SecureString extension methods in SonarQube.Client.Helpers.SecureStringHelper throw for - // nulls - if (expectedRawPassword == null) - { - actualPassword.Should().BeNull(); - } - else - { - actualPassword.ToUnsecureString().Should().Be(expectedRawPassword); - } - } - #endregion Helpers - } -} diff --git a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs index ebefc0d3ce..a5f24694aa 100644 --- a/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs +++ b/src/ConnectedMode.UnitTests/Binding/UnintrusiveBindingControllerTests.cs @@ -37,40 +37,32 @@ public class UnintrusiveBindingControllerTests private static readonly UsernameAndPasswordCredentials ValidToken = new("TOKEN", new SecureString()); private static readonly BoundServerProject AnyBoundProject = new("any", "any", new ServerConnection.SonarCloud("any", credentials: ValidToken)); private IActiveSolutionChangedHandler activeSolutionChangedHandler; - private IBindingProcess bindingProcess; - private IBindingProcessFactory bindingProcessFactory; private ISonarQubeService sonarQubeService; private UnintrusiveBindingController testSubject; private ISolutionBindingRepository solutionBindingRepository; + private IConfigurationPersister configurationPersister; [TestInitialize] public void TestInitialize() { - CreateBindingProcessFactory(); sonarQubeService = Substitute.For(); activeSolutionChangedHandler = Substitute.For(); solutionBindingRepository = Substitute.For(); - testSubject = new UnintrusiveBindingController(bindingProcessFactory, sonarQubeService, activeSolutionChangedHandler, solutionBindingRepository); + configurationPersister = Substitute.For(); + testSubject = new UnintrusiveBindingController(sonarQubeService, activeSolutionChangedHandler, solutionBindingRepository, configurationPersister); } [TestMethod] public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - [TestMethod] - public void MefCtor_IUnintrusiveBindingController_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - [TestMethod] public void MefCtor_IBindingController_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport() + ); [TestMethod] public async Task BindAsync_EstablishesConnection() @@ -102,17 +94,13 @@ public async Task BindAsync_NotifiesBindingChanged() } [TestMethod] - public async Task BindAsync_CallsBindingProcessInOrder() + public async Task BindAsync_PersistsBindingInformation() { var cancellationToken = CancellationToken.None; - await testSubject.BindAsync(AnyBoundProject, null, cancellationToken); + await testSubject.BindAsync(AnyBoundProject, cancellationToken); - Received.InOrder(() => - { - bindingProcessFactory.Create(Arg.Is(b => b.ProjectToBind == AnyBoundProject)); - bindingProcess.DownloadQualityProfileAsync(null, cancellationToken); - }); + configurationPersister.Received(1).Persist(AnyBoundProject); } [TestMethod] @@ -152,12 +140,4 @@ public void Unbind_ReturnsResultOfDeletedBinding(bool expectedResult) result.Should().Be(expectedResult); } - - private void CreateBindingProcessFactory() - { - bindingProcess ??= Substitute.For(); - - bindingProcessFactory = Substitute.For(); - bindingProcessFactory.Create(Arg.Any()).Returns(bindingProcess); - } } diff --git a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs index 428eb7e774..fb5c37080e 100644 --- a/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs +++ b/src/ConnectedMode.UnitTests/Migration/ConnectedModeMigrationTests.cs @@ -49,7 +49,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), @@ -255,27 +255,26 @@ public async Task Migrate_ThrowsCritical_NotHandled() [TestMethod] public async Task Migrate_CallBindAsync() { - var unintrusiveBindingController = new Mock(); + var configurationPersister = new Mock(); var cancellationToken = CancellationToken.None; - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object); + var testSubject = CreateTestSubject(configurationPersister: configurationPersister.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, cancellationToken); - unintrusiveBindingController.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), cancellationToken), Times.Once); + configurationPersister.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] public async Task Migrate_BoundProjectCanNotBeConvertedToServerConnection_LogsAndDoesB() { var storedConnection = new ServerConnection.SonarQube(AnyBoundProject.ServerUri); - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); MockIServerConnectionsRepositoryTryGet(serverConnectionsRepositoryMock, storedConnection.Id, storedConnection); var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + var testSubject = CreateTestSubject(configurationPersister: configurationPersisterMock.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); @@ -283,21 +282,20 @@ public async Task Migrate_BoundProjectCanNotBeConvertedToServerConnection_LogsAn serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(storedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(It.IsAny())), Times.Never); solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] public async Task Migrate_ConnectionExists_EstablishesBinding() { var storedConnection = new ServerConnection.SonarQube(AnyBoundProject.ServerUri); - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); MockIServerConnectionsRepositoryTryGet(serverConnectionsRepositoryMock, storedConnection.Id, storedConnection); var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + var testSubject = CreateTestSubject(configurationPersister: configurationPersisterMock.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); @@ -305,20 +303,19 @@ public async Task Migrate_ConnectionExists_EstablishesBinding() serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(storedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(It.IsAny())), Times.Never); solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBinding() { - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var convertedConnection = AnyBoundProject.FromBoundSonarQubeProject(); var serverConnectionsRepositoryMock = CreateServerConnectionsRepositoryMock(); var solutionInfoProviderMock = CreateSolutionInfoProviderMock(); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + var testSubject = CreateTestSubject(configurationPersister: configurationPersisterMock.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object, solutionInfoProvider: solutionInfoProviderMock.Object); serverConnectionsRepositoryMock.Setup(x => x.TryAdd(IsExpectedServerConnection(convertedConnection))).Returns(true); @@ -327,10 +324,9 @@ public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBin serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); solutionInfoProviderMock.Verify(mock => mock.GetSolutionNameAsync(), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -340,21 +336,20 @@ public async Task Migrate_ConnectionDoesNotExist_AddsConnectionAndEstablishesBin [TestMethod] public async Task Migrate_ConnectionDoesNotExist_CannotAdd_DoesNotCreateConnection() { - var unintrusiveBindingController = new Mock(); + var configurationPersister = new Mock(); var convertedConnection = AnyBoundProject.FromBoundSonarQubeProject(); var serverConnectionsRepositoryMock = new Mock(); serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); serverConnectionsRepositoryMock.Setup(x => x.TryAdd(convertedConnection)).Returns(false); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); + var testSubject = CreateTestSubject(configurationPersister: configurationPersister.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); - unintrusiveBindingController.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersister.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -364,21 +359,20 @@ public async Task Migrate_ConnectionDoesNotExist_CannotAdd_DoesNotCreateConnecti [TestMethod] public async Task Migrate_ConnectionDoesNotExist_Throws_DoesNotCreateConnection() { - var unintrusiveBindingController = new Mock(); + var configurationPersister = new Mock(); var convertedConnection = AnyBoundProject.FromBoundSonarQubeProject(); var serverConnectionsRepositoryMock = new Mock(); serverConnectionsRepositoryMock.Setup(x => x.ConnectionsFileExists()).Returns(true); serverConnectionsRepositoryMock.Setup(x => x.TryAdd(convertedConnection)).Throws(new Exception()); - var testSubject = CreateTestSubject(unintrusiveBindingController: unintrusiveBindingController.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); + var testSubject = CreateTestSubject(configurationPersister: configurationPersister.Object, serverConnectionsRepository: serverConnectionsRepositoryMock.Object); await testSubject.MigrateAsync(AnyBoundProject, Mock.Of>(), false, CancellationToken.None); serverConnectionsRepositoryMock.Verify(mock => mock.TryGet(convertedConnection.Id, out It.Ref.IsAny), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(IsExpectedServerConnection(convertedConnection)), Times.Once); - unintrusiveBindingController.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersister.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -387,13 +381,13 @@ public async Task Migrate_ConnectionDoesNotExist_Throws_DoesNotCreateConnection( [TestMethod] public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_DoesNotMigrateServerConnection() { - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = new Mock(); var bindingPathProvider = new Mock(); var logger = new Mock(); var testSubject = CreateTestSubject( serverConnectionsRepository: serverConnectionsRepositoryMock.Object, - unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + configurationPersister: configurationPersisterMock.Object, unintrusiveBindingPathProvider: bindingPathProvider.Object, logger: logger.Object); serverConnectionsRepositoryMock.Setup(mock => mock.ConnectionsFileExists()).Returns(false); @@ -403,10 +397,9 @@ public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_Doe logger.Verify(x => x.WriteLine(MigrationStrings.ConnectionsJson_DoesNotExist), Times.Once); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(It.IsAny()), Times.Never); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } /// @@ -415,13 +408,13 @@ public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNewBindingsExist_Doe [TestMethod] public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNoNewBindingsExist_MigratesConnection() { - var unintrusiveBindingControllerMock = new Mock(); + var configurationPersisterMock = new Mock(); var serverConnectionsRepositoryMock = new Mock(); var bindingPathProvider = new Mock(); var logger = new Mock(); var testSubject = CreateTestSubject( serverConnectionsRepository: serverConnectionsRepositoryMock.Object, - unintrusiveBindingController: unintrusiveBindingControllerMock.Object, + configurationPersister: configurationPersisterMock.Object, unintrusiveBindingPathProvider: bindingPathProvider.Object, logger: logger.Object); serverConnectionsRepositoryMock.Setup(mock => mock.ConnectionsFileExists()).Returns(false); @@ -431,10 +424,9 @@ public async Task Migrate_ConnectionsJsonFileDoesNotExistAndNoNewBindingsExist_M logger.Verify(x => x.WriteLine(MigrationStrings.ConnectionsJson_DoesNotExist), Times.Never); serverConnectionsRepositoryMock.Verify(mock => mock.TryAdd(It.IsAny()), Times.Once); - unintrusiveBindingControllerMock.Verify( - x => x.BindAsync( - It.Is(proj => IsExpectedBoundServerProject(proj)), - It.IsAny>(), It.IsAny()), Times.Once); + configurationPersisterMock.Verify( + x => x.Persist( + It.Is(proj => IsExpectedBoundServerProject(proj))), Times.Once); } [TestMethod] @@ -466,7 +458,7 @@ private static ConnectedModeMigration CreateTestSubject( IVsAwareFileSystem fileSystem = null, IMigrationSettingsProvider settingsProvider = null, ISonarQubeService sonarQubeService = null, - IUnintrusiveBindingController unintrusiveBindingController = null, + IConfigurationPersister configurationPersister = null, ISharedBindingConfigProvider sharedBindingConfigProvider = null, ILogger logger = null, IThreadHandling threadHandling = null, @@ -478,7 +470,7 @@ private static ConnectedModeMigration CreateTestSubject( fileCleaner ??= Mock.Of(); fileSystem ??= Mock.Of(); sonarQubeService ??= Mock.Of(); - unintrusiveBindingController ??= Mock.Of(); + configurationPersister ??= Mock.Of(); settingsProvider ??= CreateSettingsProvider(DefaultTestLegacySettings).Object; sharedBindingConfigProvider ??= Mock.Of(); solutionInfoProvider ??= CreateSolutionInfoProviderMock().Object; @@ -493,7 +485,7 @@ private static ConnectedModeMigration CreateTestSubject( fileCleaner, fileSystem, sonarQubeService, - unintrusiveBindingController, + configurationPersister, sharedBindingConfigProvider, logger, threadHandling, diff --git a/src/ConnectedMode/Binding/BindingProcessImpl.cs b/src/ConnectedMode/Binding/BindingProcessImpl.cs deleted file mode 100644 index 4386966c7b..0000000000 --- a/src/ConnectedMode/Binding/BindingProcessImpl.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding -{ - internal class BindingProcessImpl : IBindingProcess - { - private readonly BindCommandArgs bindingArgs; - private readonly IQualityProfileDownloader qualityProfileDownloader; - private readonly ILogger logger; - - public BindingProcessImpl( - BindCommandArgs bindingArgs, - IQualityProfileDownloader qualityProfileDownloader, - ILogger logger) - { - this.bindingArgs = bindingArgs ?? throw new ArgumentNullException(nameof(bindingArgs)); - this.qualityProfileDownloader = qualityProfileDownloader ?? throw new ArgumentNullException(nameof(qualityProfileDownloader)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - #region IBindingTemplate methods - - public async Task DownloadQualityProfileAsync(IProgress progress, CancellationToken cancellationToken) - { - try - { - await qualityProfileDownloader.UpdateAsync(bindingArgs.ProjectToBind, progress, cancellationToken); - // ignore the UpdateAsync result, as the return value of false indicates error, rather than lack of changes - return true; - } - catch (InvalidOperationException e) - { - logger.LogVerbose($"[{nameof(BindingProcessImpl)}] {e}"); - } - - return false; - } - - #endregion - } -} diff --git a/src/ConnectedMode/Binding/IBindingProcess.cs b/src/ConnectedMode/Binding/IBindingProcess.cs deleted file mode 100644 index c94b5b50bb..0000000000 --- a/src/ConnectedMode/Binding/IBindingProcess.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding -{ - /// - /// Encapsulates the set of operations carried out in sequence by the binding workflow. - /// The operations are expected to be called in the order in which they are defined in this file. - /// - /// This interface as extracted from the class to reduce - /// the complexity of that class and simplify testing. The class is responsible - /// for handling threading and progress reporting in the UI. This class is responsible for the - /// making the changes to projects and files. - internal interface IBindingProcess - { - Task DownloadQualityProfileAsync(IProgress progress, CancellationToken cancellationToken); - } -} diff --git a/src/ConnectedMode/Binding/IBindingProcessFactory.cs b/src/ConnectedMode/Binding/IBindingProcessFactory.cs deleted file mode 100644 index dff4b88da2..0000000000 --- a/src/ConnectedMode/Binding/IBindingProcessFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding -{ - /// - /// Factory to create a new binding process - /// - internal interface IBindingProcessFactory - { - IBindingProcess Create(BindCommandArgs bindingArgs); - } - - [Export(typeof(IBindingProcessFactory))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class BindingProcessFactory : IBindingProcessFactory - { - private readonly IQualityProfileDownloader qualityProfileDownloader; - private readonly ILogger logger; - - [ImportingConstructor] - public BindingProcessFactory( - IQualityProfileDownloader qualityProfileDownloader, - ILogger logger) - { - this.qualityProfileDownloader = qualityProfileDownloader; - this.logger = logger; - } - - public IBindingProcess Create(BindCommandArgs bindingArgs) - { - return new BindingProcessImpl(bindingArgs, - qualityProfileDownloader, - logger); - } - } -} diff --git a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs index 84ae87eab9..3095191ca4 100644 --- a/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs +++ b/src/ConnectedMode/Binding/IUnintrusiveBindingController.cs @@ -34,31 +34,25 @@ public interface IBindingController bool Unbind(string localBindingKey); } - internal interface IUnintrusiveBindingController - { - Task BindAsync(BoundServerProject project, IProgress progress, CancellationToken token); - } - [Export(typeof(IBindingController))] - [Export(typeof(IUnintrusiveBindingController))] [PartCreationPolicy(CreationPolicy.NonShared)] - internal class UnintrusiveBindingController : IUnintrusiveBindingController, IBindingController + internal class UnintrusiveBindingController : IBindingController { - private readonly IBindingProcessFactory bindingProcessFactory; private readonly ISonarQubeService sonarQubeService; private readonly IActiveSolutionChangedHandler activeSolutionChangedHandler; + private readonly IConfigurationPersister configurationPersister; private readonly ISolutionBindingRepository solutionBindingRepository; [ImportingConstructor] public UnintrusiveBindingController( - IBindingProcessFactory bindingProcessFactory, ISonarQubeService sonarQubeService, IActiveSolutionChangedHandler activeSolutionChangedHandler, - ISolutionBindingRepository solutionBindingRepository) + ISolutionBindingRepository solutionBindingRepository, + IConfigurationPersister configurationPersister) { - this.bindingProcessFactory = bindingProcessFactory; this.sonarQubeService = sonarQubeService; this.activeSolutionChangedHandler = activeSolutionChangedHandler; + this.configurationPersister = configurationPersister; this.solutionBindingRepository = solutionBindingRepository; } @@ -66,16 +60,10 @@ public async Task BindAsync(BoundServerProject project, CancellationToken cancel { var connectionInformation = new ConnectionInformation(project.ServerConnection.ServerUri, project.ServerConnection.Credentials); await sonarQubeService.ConnectAsync(connectionInformation, cancellationToken); - await BindAsync(project, null, cancellationToken); + configurationPersister.Persist(project); activeSolutionChangedHandler.HandleBindingChange(); } - public async Task BindAsync(BoundServerProject project, IProgress progress, CancellationToken token) - { - var bindingProcess = CreateBindingProcess(project); - await bindingProcess.DownloadQualityProfileAsync(progress, token); - } - public bool Unbind(string localBindingKey) { var bindingDeleted = solutionBindingRepository.DeleteBinding(localBindingKey); @@ -86,12 +74,5 @@ public bool Unbind(string localBindingKey) } return bindingDeleted; } - - private IBindingProcess CreateBindingProcess(BoundServerProject project) - { - var bindingProcess = bindingProcessFactory.Create(new BindCommandArgs(project)); - - return bindingProcess; - } } } diff --git a/src/ConnectedMode/Migration/ConnectedModeMigration.cs b/src/ConnectedMode/Migration/ConnectedModeMigration.cs index 6b98538a15..d45b2de1dd 100644 --- a/src/ConnectedMode/Migration/ConnectedModeMigration.cs +++ b/src/ConnectedMode/Migration/ConnectedModeMigration.cs @@ -42,7 +42,7 @@ private sealed class ChangedFiles : List> private readonly IFileCleaner fileCleaner; private readonly IVsAwareFileSystem fileSystem; private readonly ISonarQubeService sonarQubeService; - private readonly IUnintrusiveBindingController unintrusiveBindingController; + private readonly IConfigurationPersister configurationPersister; private readonly ISharedBindingConfigProvider sharedBindingConfigProvider; private readonly ILogger logger; private readonly IThreadHandling threadHandling; @@ -60,7 +60,7 @@ public ConnectedModeMigration( IFileCleaner fileCleaner, IVsAwareFileSystem fileSystem, ISonarQubeService sonarQubeService, - IUnintrusiveBindingController unintrusiveBindingController, + IConfigurationPersister configurationPersister, ISharedBindingConfigProvider sharedBindingConfigProvider, ILogger logger, IThreadHandling threadHandling, @@ -73,7 +73,7 @@ public ConnectedModeMigration( this.fileCleaner = fileCleaner; this.fileSystem = fileSystem; this.sonarQubeService = sonarQubeService; - this.unintrusiveBindingController = unintrusiveBindingController; + this.configurationPersister = configurationPersister; this.sharedBindingConfigProvider = sharedBindingConfigProvider; this.logger = logger; @@ -146,11 +146,8 @@ private async Task MigrateImplAsync( progress?.Report(new MigrationProgress(0, 1, "Creating new binding files ...", false)); logger.WriteLine(MigrationStrings.Process_ProcessingNewBinding); - var progressAdapter = new FixedStepsProgressToMigrationProgressAdapter(progress); var serverConnection = GetServerConnectionWithMigration(oldBinding); - await unintrusiveBindingController.BindAsync(oldBinding.FromBoundSonarQubeProject(await solutionInfoProvider.GetSolutionNameAsync(), serverConnection), - progressAdapter, - token); + configurationPersister.Persist(oldBinding.FromBoundSonarQubeProject(await solutionInfoProvider.GetSolutionNameAsync(), serverConnection)); // Now make all of the files changes required to remove the legacy settings // i.e. update project files and delete .sonarlint folder From bce755b4c05b3a455121276a152e55592fc5f9aa Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 5 Sep 2025 10:18:34 +0200 Subject: [PATCH 24/38] SLVS-2514 Introduce DocumentUpdated event (#6401) --- src/Core/DocumentEvents.cs | 16 ++------ .../Analysis/DocumentEventsHandlerTests.cs | 40 +++++++++---------- .../SonarLintTagger/TaggerProviderTests.cs | 39 ++++++++++++++++-- .../TextBufferIssueTrackerTests.cs | 7 ++-- .../Analysis/DocumentEventsHandler.cs | 4 +- .../SonarLintTagger/TaggerProvider.cs | 15 +++++-- 6 files changed, 75 insertions(+), 46 deletions(-) diff --git a/src/Core/DocumentEvents.cs b/src/Core/DocumentEvents.cs index 5f89165288..834a12fa3b 100644 --- a/src/Core/DocumentEvents.cs +++ b/src/Core/DocumentEvents.cs @@ -22,18 +22,9 @@ namespace SonarLint.VisualStudio.Core; -public class DocumentEventArgs(Document document) : EventArgs +public class DocumentEventArgs(Document document, string content = null) : EventArgs { public Document Document { get; } = document; -} - -public class DocumentSavedEventArgs(Document document, string newContent) : DocumentEventArgs(document) -{ - public string NewContent { get; } = newContent; -} - -public class DocumentOpenedEventArgs(Document document, string content) : DocumentEventArgs(document) -{ public string Content { get; } = content; } @@ -54,8 +45,9 @@ public class Document(string fullPath, IEnumerable detectedLan public interface IDocumentTracker { event EventHandler DocumentClosed; - event EventHandler DocumentOpened; - event EventHandler DocumentSaved; + event EventHandler DocumentOpened; + event EventHandler DocumentSaved; + event EventHandler DocumentUpdated; /// /// Raised when an opened document is renamed /// diff --git a/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs b/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs index e67ff3531d..2f9cee5c37 100644 --- a/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs +++ b/src/Integration.Vsix.UnitTests/Analysis/DocumentEventsHandlerTests.cs @@ -103,9 +103,9 @@ public void Ctor_InitializesInCorrectOrder() testSubject.InitializationProcessor.InitializeAsync(); activeConfigScopeTracker.CurrentConfigurationScopeChanged += Arg.Any>(); activeCompilationDatabaseTracker.DatabaseChanged += Arg.Any(); - documentTracker.DocumentOpened += Arg.Any>(); + documentTracker.DocumentOpened += Arg.Any>(); documentTracker.DocumentClosed += Arg.Any>(); - documentTracker.DocumentSaved += Arg.Any>(); + documentTracker.DocumentSaved += Arg.Any>(); documentTracker.OpenDocumentRenamed += Arg.Any>(); documentTracker.GetOpenDocuments(); // the remaining logic is tested in other tests slCoreServiceProvider.TryGetTransientService(out Arg.Any()); @@ -177,7 +177,7 @@ public void Ctor_SlCoreServiceNotAvailable_WithOpenCFamilyFiles_AddsToCompilatio public void DocumentOpened_CFamily_VcxProject_AddFileToCompilationDbAndNotifiesSlCore() { CreateAndInitializeTestSubject(); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -191,7 +191,7 @@ public void DocumentOpened_CFamily_CMakeProject_DoesNotAddFileToVcxCompilationDb { CreateAndInitializeTestSubject(); MockCompilationDatabaseType(CompilationDatabaseType.CMake); - var args = new DocumentOpenedEventArgs(CFamilyDocument, "content"); + var args = new DocumentEventArgs(CFamilyDocument, "content"); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -260,7 +260,7 @@ public void OpenDocumentRenamed_CFamily_CMakeProject_DoesNotRenameFileInCompilat public void DocumentOpened_NonCFamily_DoesNotAddFileToVcxCompilationDbButNotifiesSlCore() { CreateAndInitializeTestSubject(); - var args = new DocumentOpenedEventArgs(NonCFamilyDocument, string.Empty); + var args = new DocumentEventArgs(NonCFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -286,7 +286,7 @@ public void DocumentClosed_NonCFamily_DoesNotRemoveFileFromCompilationDbButNotif public void DocumentSaved_CFamily_VcxProject_AddFileToCompilationDb() { CreateAndInitializeTestSubject(); - var args = new DocumentSavedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -298,7 +298,7 @@ public void DocumentSaved_CFamily_CmakeProject_DoesNotAddFileToVcxCompilationDb( { CreateAndInitializeTestSubject(); MockCompilationDatabaseType(CompilationDatabaseType.CMake); - var args = new DocumentSavedEventArgs(CFamilyDocument, "content"); + var args = new DocumentEventArgs(CFamilyDocument, "content"); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -323,7 +323,7 @@ public void OpenDocumentRenamed_NonCFamily_DoesNotRenameFileFromCompilationDbBut [TestMethod] public void DocumentSaved_NonCFamily_DoesNothing() { - var args = new DocumentSavedEventArgs(NonCFamilyDocument, string.Empty); + var args = new DocumentEventArgs(NonCFamilyDocument, string.Empty); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -336,7 +336,7 @@ public void DocumentOpened_ExecutesOnBackgroundThread() { CreateAndInitializeTestSubject(); threadHandling.ClearReceivedCalls(); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -391,7 +391,7 @@ public void DocumentSaved_ExecutesOnBackgroundThread() { CreateAndInitializeTestSubject(); threadHandling.ClearReceivedCalls(); - var args = new DocumentSavedEventArgs(CFamilyDocument, "using System;"); + var args = new DocumentEventArgs(CFamilyDocument, "using System;"); documentTracker.DocumentSaved += Raise.EventWith(documentTracker, args); @@ -407,7 +407,7 @@ public void DocumentOpened_SlCoreServiceNotAvailable_DoesNotNotifySlCoreAndLogs( { CreateAndInitializeTestSubject(); MockFileRpcService(service: null, succeeds: false); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -450,7 +450,7 @@ public void DocumentOpened_CurrentConfigScopeIsNull_DoesNotNotifySlCoreAndLogs() { CreateAndInitializeTestSubject(); MockCurrentConfigScope(configurationScope: null); - var args = new DocumentOpenedEventArgs(CFamilyDocument, string.Empty); + var args = new DocumentEventArgs(CFamilyDocument, string.Empty); documentTracker.DocumentOpened += Raise.EventWith(documentTracker, args); @@ -501,12 +501,12 @@ public void Dispose_BeforeInitialization_DoesNotUnsubscribe() activeConfigScopeTracker.DidNotReceive().CurrentConfigurationScopeChanged -= Arg.Any>(); activeCompilationDatabaseTracker.DidNotReceive().DatabaseChanged += Arg.Any(); activeCompilationDatabaseTracker.DidNotReceive().DatabaseChanged -= Arg.Any(); - documentTracker.DidNotReceive().DocumentOpened += Arg.Any>(); - documentTracker.DidNotReceive().DocumentOpened -= Arg.Any>(); + documentTracker.DidNotReceive().DocumentOpened += Arg.Any>(); + documentTracker.DidNotReceive().DocumentOpened -= Arg.Any>(); documentTracker.DidNotReceive().DocumentClosed += Arg.Any>(); documentTracker.DidNotReceive().DocumentClosed -= Arg.Any>(); - documentTracker.DidNotReceive().DocumentSaved += Arg.Any>(); - documentTracker.DidNotReceive().DocumentSaved -= Arg.Any>(); + documentTracker.DidNotReceive().DocumentSaved += Arg.Any>(); + documentTracker.DidNotReceive().DocumentSaved -= Arg.Any>(); documentTracker.DidNotReceive().OpenDocumentRenamed += Arg.Any>(); documentTracker.DidNotReceive().OpenDocumentRenamed -= Arg.Any>(); } @@ -523,8 +523,8 @@ public void Dispose_AfterInitialization_Unsubscribes() activeConfigScopeTracker.Received(1).CurrentConfigurationScopeChanged -= Arg.Any>(); activeCompilationDatabaseTracker.Received(1).DatabaseChanged -= Arg.Any(); documentTracker.Received(1).DocumentClosed -= Arg.Any>(); - documentTracker.Received(1).DocumentOpened -= Arg.Any>(); - documentTracker.Received(1).DocumentSaved -= Arg.Any>(); + documentTracker.Received(1).DocumentOpened -= Arg.Any>(); + documentTracker.Received(1).DocumentSaved -= Arg.Any>(); documentTracker.Received(1).OpenDocumentRenamed -= Arg.Any>(); } @@ -581,7 +581,6 @@ public void ActiveConfigScopeTracker_CurrentConfigurationScopeChanged_SlCoreServ logger.Received(1).LogVerbose(SLCoreStrings.ServiceProviderNotInitialized); } - [TestMethod] public void ActiveCompilationDatabaseTracker_DatabaseChanged_UpdatesCompilationDatabase() { @@ -684,5 +683,6 @@ private void MockThreadHandling() => private void MockCurrentConfigScope(ConfigurationScope configurationScope) => activeConfigScopeTracker.Current.Returns(configurationScope); - private void MockCompilationDatabaseType(CompilationDatabaseType? type) => activeCompilationDatabaseTracker.CurrentDatabase.Returns(type is null ? null : new CompilationDatabaseInfo("any", type.Value)); + private void MockCompilationDatabaseType(CompilationDatabaseType? type) => + activeCompilationDatabaseTracker.CurrentDatabase.Returns(type is null ? null : new CompilationDatabaseInfo("any", type.Value)); } diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs index b27493a452..0f387ea322 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs @@ -341,7 +341,7 @@ public void FilterIssueTrackersByPath_WithPaths_AllMatched_AllTrackersReturned() [TestMethod] public void AddIssueTracker_RaisesEvent() { - var eventHandler = Substitute.For>(); + var eventHandler = Substitute.For>(); var filePath = "file1.txt"; var content = "some text"; testSubject.DocumentOpened += eventHandler; @@ -349,7 +349,7 @@ public void AddIssueTracker_RaisesEvent() testSubject.AddIssueTracker(issueTracker); - eventHandler.Received(1).Invoke(testSubject, Arg.Is(e => + eventHandler.Received(1).Invoke(testSubject, Arg.Is(e => e.Document.FullPath == filePath && e.Document.DetectedLanguages == DetectedLanguagesJsTs && e.Content == content)); @@ -384,7 +384,7 @@ public void IssueTracker_DocumentClosed_RaiseEvent() [TestMethod] public void IssueTracker_DocumentSaved_RaiseEvent() { - var eventHandler = Substitute.For>(); + var eventHandler = Substitute.For>(); var fileName = "anyname.js"; var doc = CreateMockedDocument(fileName, DetectedLanguagesJsTs); testSubject.DocumentSaved += eventHandler; @@ -392,7 +392,7 @@ public void IssueTracker_DocumentSaved_RaiseEvent() CreateTaggerForDocument(doc); RaiseFileEvent(doc, FileActionTypes.ContentSavedToDisk); - eventHandler.Received(1).Invoke(Arg.Any(), Arg.Is(x => x.Document.FullPath == fileName && x.Document.DetectedLanguages == DetectedLanguagesJsTs)); + eventHandler.Received(1).Invoke(Arg.Any(), Arg.Is(x => x.Document.FullPath == fileName && x.Document.DetectedLanguages == DetectedLanguagesJsTs)); } [TestMethod] @@ -425,6 +425,37 @@ public void IssueTracker_DocumentRename_RaiseEvent() Arg.Is(x => x.Document.FullPath == newName && x.OldFilePath == oldName && x.Document.DetectedLanguages == DetectedLanguagesJsTs)); } + [TestMethod] + public void IssueTracker_DocumentUpdated_RaiseEvent() + { + var eventHandler = Substitute.For>(); + var fileName = "anyname.js"; + string content = "new content"; + var doc = CreateMockedDocument(fileName, DetectedLanguagesJsTs); + testSubject.DocumentUpdated += eventHandler; + + CreateTaggerForDocument(doc); + testSubject.OnDocumentUpdated(fileName, content, DetectedLanguagesJsTs); + + eventHandler.Received(1).Invoke(Arg.Any(), Arg.Is(x => x.Document.FullPath == fileName + && x.Document.DetectedLanguages == DetectedLanguagesJsTs + && x.Content == content)); + } + + [TestMethod] + public void IssueTracker_DocumentUpdated_AddsNewFileToFileTracker() + { + var filePath = "anyname.js"; + string content = "new content"; + var doc = CreateMockedDocument(filePath, DetectedLanguagesJsTs, content: content); + CreateTaggerForDocument(doc); + mockFileTracker.ClearReceivedCalls(); + + testSubject.OnDocumentUpdated(filePath, content, DetectedLanguagesJsTs); + + mockFileTracker.Received(1).AddFiles(new SourceFile(filePath, encoding: null, content)); + } + [TestMethod] public void GetOpenDocuments_ReturnsAmountOfIssueTrackers() { diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs index f12f7bb93b..da206175c5 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs @@ -26,7 +26,6 @@ using Microsoft.VisualStudio.Utilities; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Initialization; using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.Infrastructure.VS.Initialization; using SonarLint.VisualStudio.Integration.Vsix; @@ -167,13 +166,13 @@ public void Dispose_RaisesEvent() [TestMethod] public void WhenFileIsSaved_DocumentSavedEventIsRaised() { - var eventHandler = Substitute.For>(); + var eventHandler = Substitute.For>(); taggerProvider.DocumentSaved += eventHandler; RaiseFileSavedEvent(mockedJavascriptDocumentFooJs); eventHandler.Received(1).Invoke(taggerProvider, - Arg.Is(x => x.Document.FullPath == mockedJavascriptDocumentFooJs.FilePath && x.Document.DetectedLanguages == javascriptLanguage)); + Arg.Is(x => x.Document.FullPath == mockedJavascriptDocumentFooJs.FilePath && x.Document.DetectedLanguages == javascriptLanguage)); } [TestMethod] @@ -195,7 +194,7 @@ public void WhenFileIsSaved_AnalysisSnapshotIsUpdated() public void WhenFileIsLoaded_EventsAreNotRaised() { var renamedEventHandler = Substitute.For>(); - var savedEventHandler = Substitute.For>(); + var savedEventHandler = Substitute.For>(); taggerProvider.OpenDocumentRenamed += renamedEventHandler; taggerProvider.DocumentSaved += savedEventHandler; diff --git a/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs b/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs index 35e4951c64..9308f8def0 100644 --- a/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs +++ b/src/Integration.Vsix/Analysis/DocumentEventsHandler.cs @@ -151,7 +151,7 @@ private void OnDocumentClosed(object sender, DocumentEventArgs args) => NotifySlCoreFileClosed(args.Document.FullPath, activeConfigScopeTracker.Current); }).Forget(); - private void OnDocumentOpened(object sender, DocumentOpenedEventArgs args) => + private void OnDocumentOpened(object sender, DocumentEventArgs args) => threadHandling.RunOnBackgroundThread(async () => { await AddFilesToCompilationDatabaseAsync(args.Document); @@ -163,7 +163,7 @@ private void OnDocumentOpened(object sender, DocumentOpenedEventArgs args) => /// Due to the fact that we can't react to project/file properties changes, regenerating the compilation database entry on file save is needed /// Additionally, this is a workaround that can deal with the renaming bug described in https://sonarsource.atlassian.net/browse/SLVS-2170 /// - private void OnDocumentSaved(object sender, DocumentSavedEventArgs args) => + private void OnDocumentSaved(object sender, DocumentEventArgs args) => threadHandling.RunOnBackgroundThread(async () => { await AddFilesToCompilationDatabaseAsync(args.Document); diff --git a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs index 111d69d854..f7335e4d5c 100644 --- a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs +++ b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs @@ -223,8 +223,9 @@ private TextBufferIssueTracker InternalCreateTextBufferIssueTracker(ITextDocumen #region IDocumentTracker methods public event EventHandler DocumentClosed; - public event EventHandler DocumentOpened; - public event EventHandler DocumentSaved; + public event EventHandler DocumentOpened; + public event EventHandler DocumentSaved; + public event EventHandler DocumentUpdated; public event EventHandler OpenDocumentRenamed; public Document[] GetOpenDocuments() @@ -247,7 +248,7 @@ public void AddIssueTracker(IIssueTracker issueTracker) NotifyFileTracker(filePath, content); // The lifetime of an issue tracker is tied to a single document. A document is opened, then a tracker is created. - DocumentOpened?.Invoke(this, new DocumentOpenedEventArgs(new Document(filePath, issueTracker.DetectedLanguages), content)); + DocumentOpened?.Invoke(this, new DocumentEventArgs(new Document(filePath, issueTracker.DetectedLanguages), content)); } public void OnOpenDocumentRenamed(string newFilePath, string oldFilePath, IEnumerable detectedLanguages) => @@ -256,7 +257,13 @@ public void OnOpenDocumentRenamed(string newFilePath, string oldFilePath, IEnume public void OnDocumentSaved(string fullPath, string newContent, IEnumerable detectedLanguages) { NotifyFileTracker(fullPath, newContent); - DocumentSaved?.Invoke(this, new DocumentSavedEventArgs(new Document(fullPath, detectedLanguages), newContent)); + DocumentSaved?.Invoke(this, new DocumentEventArgs(new Document(fullPath, detectedLanguages), newContent)); + } + + public void OnDocumentUpdated(string fullPath, string newContent, IEnumerable detectedLanguages) + { + NotifyFileTracker(fullPath, newContent); + DocumentUpdated?.Invoke(this, new DocumentEventArgs(new Document(fullPath, detectedLanguages), newContent)); } public void OnDocumentClosed(IIssueTracker issueTracker) From dcd485f9c38010d3c362edcdcd015babbbda97b4 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Fri, 5 Sep 2025 15:56:33 +0200 Subject: [PATCH 25/38] SLVS-2430 Trigger analysis on pressed key with debounce (#6402) --- .../SonarLintTagger/TaggerProviderTests.cs | 10 ++- .../TaskExecutorWithDebounceFactoryTest.cs | 55 ++++++++++++ .../TaskExecutorWithDebounceTest.cs | 89 +++++++++++++++++++ .../TextBufferIssueTrackerTests.cs | 72 +++++++++++++-- .../SonarLintTagger/TaggerProvider.cs | 53 ++++++----- .../TaskExecutorWithDebounce.cs | 80 +++++++++++++++++ .../SonarLintTagger/TextBufferIssueTracker.cs | 53 +++++++---- 7 files changed, 365 insertions(+), 47 deletions(-) create mode 100644 src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs create mode 100644 src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs create mode 100644 src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs index 0f387ea322..fd20a77f96 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs @@ -31,6 +31,7 @@ using SonarLint.VisualStudio.Integration.Vsix; using SonarLint.VisualStudio.Integration.Vsix.Analysis; using SonarLint.VisualStudio.Integration.Vsix.ErrorList; +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; using SonarLint.VisualStudio.IssueVisualization.Editor; using SonarLint.VisualStudio.IssueVisualization.Editor.LanguageDetection; @@ -57,6 +58,7 @@ public class TaggerProviderTests private IAnalyzer analyzer; private IInitializationProcessorFactory initializationProcessorFactory; private IThreadHandling threadHandling; + private ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; private static readonly AnalysisLanguage[] DetectedLanguagesJsTs = [AnalysisLanguage.TypeScript, AnalysisLanguage.Javascript]; @@ -90,6 +92,8 @@ public void SetUp() threadHandling = Substitute.ForPartsOf(); + taskExecutorWithDebounceFactory = Substitute.For(); + testSubject = CreateAndInitializeTestSubject(); } @@ -121,7 +125,8 @@ private static Export[] GetRequiredExports() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport() + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), ]; #endregion MEF tests @@ -147,6 +152,7 @@ public void CreateTagger_should_create_tracker_when_tagger_is_created() tagger.Should().NotBeNull(); VerifyCreateIssueConsumerWasCalled(doc); + taskExecutorWithDebounceFactory.Received(1).Create(debounceMilliseconds: TimeSpan.FromMilliseconds(500)); } [TestMethod] @@ -611,7 +617,7 @@ private TaggerProvider CreateAndInitializeTestSubject() var taggerProvider = new TaggerProvider( mockSonarErrorDataSource, dummyDocumentFactoryService, serviceProvider, mockSonarLanguageRecognizer, mockAnalysisRequester, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, - mockTaggableBufferIndicator, mockFileTracker, analyzer, logger, initializationProcessorFactory); + mockTaggableBufferIndicator, mockFileTracker, analyzer, logger, initializationProcessorFactory, taskExecutorWithDebounceFactory); taggerProvider.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); return taggerProvider; } diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs new file mode 100644 index 0000000000..40aac54c3e --- /dev/null +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs @@ -0,0 +1,55 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; + +[TestClass] +public class TaskExecutorWithDebounceFactoryTest +{ + private TaskExecutorWithDebounceFactory testSubject; + private IAsyncLockFactory asyncLockFactory; + + [TestInitialize] + public void TestInitialize() + { + asyncLockFactory = Substitute.For(); + testSubject = new TaskExecutorWithDebounceFactory(asyncLockFactory); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Create_ShouldReturnInstance() + { + var result = testSubject.Create(TimeSpan.FromMilliseconds(1)); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + } +} diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs new file mode 100644 index 0000000000..2cc88aa22f --- /dev/null +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs @@ -0,0 +1,89 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; + +[TestClass] +public class TaskExecutorWithDebounceTest +{ + private readonly TimeSpan debounceTimeInMs = TimeSpan.FromMilliseconds(100); + private IAsyncLock asyncLock; + private IAsyncLockFactory asyncLockFactory; + private TaskExecutorWithDebounce testSubject; + + [TestInitialize] + public void TestInitialize() + { + asyncLockFactory = Substitute.For(); + asyncLock = Substitute.For(); + asyncLockFactory.Create().Returns(asyncLock); + testSubject = new TaskExecutorWithDebounce(asyncLockFactory, debounceTimeInMs); + } + + [TestMethod] + public async Task DebounceAsync_ExecutesTaskWithDebounce() + { + var currentState = new TestData { Value = 1 }; + var tcs = new TaskCompletionSource(); + var stopwatch = Stopwatch.StartNew(); + + testSubject.DebounceAsync(() => + { + UpdateState(currentState, 2, tcs); + stopwatch.Stop(); + }).Forget(); + await tcs.Task; + + asyncLock.Received(1).AcquireAsync().IgnoreAwaitForAssert(); + currentState.Value.Should().Be(2); + stopwatch.ElapsedMilliseconds.Should().BeGreaterOrEqualTo(debounceTimeInMs.Milliseconds); + } + + [TestMethod] + public async Task DebounceAsync_MultipleTimes_UpdatesWithLatestState() + { + var currentState = new TestData { Value = 1 }; + var tcs = new TaskCompletionSource(); + + testSubject.DebounceAsync(() => UpdateState(currentState, 2)).Forget(); + testSubject.DebounceAsync(() => UpdateState(currentState, 3)).Forget(); + testSubject.DebounceAsync(() => UpdateState(currentState, 4, tcs)).Forget(); + await tcs.Task; + + asyncLock.Received(3).AcquireAsync().IgnoreAwaitForAssert(); + currentState.Value.Should().Be(4); + } + + private static void UpdateState(TestData date, int newValue, TaskCompletionSource taskCompletionSource = null) + { + date.Value = newValue; + taskCompletionSource?.SetResult(1); + } + + private record TestData + { + public int Value { get; set; } + } +} diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs index da206175c5..09fe5b0d38 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs @@ -56,6 +56,8 @@ public class TextBufferIssueTrackerTests private IIssueConsumerFactory issueConsumerFactory; private IIssueConsumerStorage issueConsumerStorage; private IIssueConsumer issueConsumer; + private ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; + private ITaskExecutorWithDebounce taskExecutorWithDebounce; [TestInitialize] public void SetUp() @@ -71,6 +73,9 @@ public void SetUp() mockDocumentTextBuffer = CreateTextBufferMock(mockTextSnapshot); mockedJavascriptDocumentFooJs = CreateDocumentMock("foo.js", mockDocumentTextBuffer); javascriptLanguage = [AnalysisLanguage.Javascript]; + taskExecutorWithDebounceFactory = Substitute.For(); + taskExecutorWithDebounce = Substitute.For(); + taskExecutorWithDebounceFactory.Create(Arg.Any()).Returns(taskExecutorWithDebounce); MockIssueConsumerFactory(mockedJavascriptDocumentFooJs, issueConsumer); testSubject = CreateTestSubject(mockedJavascriptDocumentFooJs); @@ -98,6 +103,7 @@ public void Ctor_RegistersEventsTrackerAndFactory() taggerProvider.ActiveTrackersForTesting.Should().BeEquivalentTo(testSubject); mockedJavascriptDocumentFooJs.Received(1).FileActionOccurred += Arg.Any>(); + ((ITextBuffer2)mockDocumentTextBuffer).Received(1).ChangedOnBackground += Arg.Any>(); // Note: the test subject isn't responsible for adding the entry to the buffer.Properties // - that's done by the TaggerProvider. @@ -150,6 +156,7 @@ public void Dispose_CleansUpEventsAndRegistrations() taggerProvider.ActiveTrackersForTesting.Should().BeEmpty(); mockedJavascriptDocumentFooJs.Received(1).FileActionOccurred -= Arg.Any>(); + ((ITextBuffer2)mockDocumentTextBuffer).Received(1).ChangedOnBackground -= Arg.Any>(); } [TestMethod] @@ -196,7 +203,6 @@ public void WhenFileIsLoaded_EventsAreNotRaised() var renamedEventHandler = Substitute.For>(); var savedEventHandler = Substitute.For>(); taggerProvider.OpenDocumentRenamed += renamedEventHandler; - taggerProvider.DocumentSaved += savedEventHandler; RaiseFileLoadedEvent(mockedJavascriptDocumentFooJs); @@ -326,6 +332,21 @@ public void UpdateAnalysisState_CriticalException_IsNotSuppressed() .WithMessage("this is a test"); } + [TestMethod] + public void OnTextBufferChangedOnBackground_UpdatesAnalysisState() + { + var eventHandler = SubscribeToDocumentSaved(); + var newContent = "new content"; + var newSnapshot = CreateTextSnapshotMock(newContent); + var newAnalysisSnapshot = new AnalysisSnapshot(mockedJavascriptDocumentFooJs.FilePath, newSnapshot); + MockTaskExecutorWithDebounce(); + CreateTestSubject(mockedJavascriptDocumentFooJs); + + RaiseTextBufferChangedOnBackground(currentTextBuffer: mockDocumentTextBuffer, newSnapshot); + + VerifyAnalysisStateUpdated(mockedJavascriptDocumentFooJs, newAnalysisSnapshot, eventHandler, newContent); + } + private void SetUpIssueConsumerStorageThrows(Exception exception) => issueConsumerStorage.When(x => x.Remove(Arg.Any())).Do(x => throw exception); private static void VerifySingletonManagerDoesNotExist(ITextBuffer buffer) => FindSingletonManagerInPropertyCollection(buffer).Should().BeNull(); @@ -372,21 +393,21 @@ private TaggerProvider CreateTaggerProvider() var analysisRequester = mockAnalysisRequester; var provider = new TaggerProvider(sonarErrorListDataSource, textDocumentFactoryService, serviceProvider, languageRecognizer, analysisRequester, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, Mock.Of(), - mockFileTracker, analyzer, logger, new InitializationProcessorFactory(Substitute.For(), new NoOpThreadHandler(), new TestLogger())); + mockFileTracker, analyzer, logger, new InitializationProcessorFactory(Substitute.For(), new NoOpThreadHandler(), new TestLogger()), taskExecutorWithDebounceFactory); return provider; } - private static ITextSnapshot CreateTextSnapshotMock() + private static ITextSnapshot CreateTextSnapshotMock(string content = TextContent) { var textSnapshot = Substitute.For(); - textSnapshot.GetText().Returns(TextContent); + textSnapshot.GetText().Returns(content); return textSnapshot; } private static ITextBuffer CreateTextBufferMock(ITextSnapshot textSnapshot) { // Text buffer with a properties collection and current snapshot - var mockTextBuffer = Substitute.For(); + var mockTextBuffer = Substitute.For(); var dummyProperties = new PropertyCollection(); mockTextBuffer.Properties.Returns(dummyProperties); @@ -446,7 +467,7 @@ private TextBufferIssueTracker CreateTestSubject(ITextDocument textDocument) private TextBufferIssueTracker CreateTestSubject(ITextDocument textDocument, ILogger logger) => new(taggerProvider, textDocument, javascriptLanguage, - mockSonarErrorDataSource, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, logger); + mockSonarErrorDataSource, vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, taskExecutorWithDebounce, logger); private void ClearIssueConsumerCalls() { @@ -454,4 +475,43 @@ private void ClearIssueConsumerCalls() issueConsumerFactory.ClearReceivedCalls(); vsProjectInfoProvider.ClearReceivedCalls(); } + + private static void RaiseTextBufferChangedOnBackground(ITextBuffer currentTextBuffer, ITextSnapshot newTextSnapshot) + { + var args = new TextContentChangedEventArgs(Substitute.For(), newTextSnapshot, EditOptions.DefaultMinimalChange, null); + ((ITextBuffer2)currentTextBuffer).ChangedOnBackground += Raise.EventWith(null, args); + } + + private EventHandler SubscribeToDocumentSaved() + { + var eventHandler = Substitute.For>(); + taggerProvider.DocumentUpdated += eventHandler; + return eventHandler; + } + + private void MockTaskExecutorWithDebounce() => + taskExecutorWithDebounce.When(x => x.DebounceAsync(Arg.Any())).Do(callInfo => + { + var action = callInfo.Arg(); + action(); + }); + + private void VerifyAnalysisStateUpdated( + ITextDocument textDocument, + AnalysisSnapshot newAnalysisSnapshot, + EventHandler eventHandler, + string newContent) + { + issueConsumerStorage.Received().Remove(textDocument.FilePath); + vsProjectInfoProvider.Received().GetDocumentProjectInfo(newAnalysisSnapshot.FilePath); + issueConsumerFactory.Received().Create(textDocument, newAnalysisSnapshot.FilePath, newAnalysisSnapshot.TextSnapshot, Arg.Any(), Arg.Any(), + Arg.Any()); + issueConsumerStorage.Received().Set(textDocument.FilePath, Arg.Any()); + issueConsumer.Received().SetIssues(textDocument.FilePath, []); + issueConsumer.Received().SetHotspots(textDocument.FilePath, []); + eventHandler.Received().Invoke(taggerProvider, + Arg.Is(x => x.Document.FullPath == textDocument.FilePath + && x.Document.DetectedLanguages == javascriptLanguage + && x.Content == newContent)); + } } diff --git a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs index f7335e4d5c..869646d8ff 100644 --- a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs +++ b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs @@ -40,10 +40,10 @@ namespace SonarLint.VisualStudio.Integration.Vsix; /// -/// Factory for the . There will be one instance of this class/VS session. +/// Factory for the . There will be one instance of this class/VS session. /// /// -/// See the README.md in this folder for more information +/// See the README.md in this folder for more information /// [Export(typeof(ITaggerProvider))] [Export(typeof(IDocumentTracker))] @@ -54,7 +54,9 @@ namespace SonarLint.VisualStudio.Integration.Vsix; internal sealed class TaggerProvider : ITaggerProvider, IRequireInitialization, IDocumentTracker, IDisposable { internal static readonly Type SingletonManagerPropertyCollectionKey = typeof(SingletonDisposableTaggerManager); + private readonly IAnalysisRequester analysisRequester; private readonly IAnalyzer analyzer; + private readonly TimeSpan debounceMilliseconds = TimeSpan.FromMilliseconds(500); private readonly IFileTracker fileTracker; private readonly IIssueConsumerFactory issueConsumerFactory; private readonly IIssueConsumerStorage issueConsumerStorage; @@ -62,21 +64,21 @@ internal sealed class TaggerProvider : ITaggerProvider, IRequireInitialization, private readonly ISet issueTrackers = new HashSet(); private readonly ISonarLanguageRecognizer languageRecognizer; - private readonly IAnalysisRequester analysisRequester; private readonly ILogger logger; private readonly object reanalysisLockObject = new(); internal readonly ISonarErrorListDataSource sonarErrorDataSource; private readonly ITaggableBufferIndicator taggableBufferIndicator; + private readonly ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; internal readonly ITextDocumentFactoryService textDocumentFactoryService; private readonly IVsProjectInfoProvider vsProjectInfoProvider; - private IVsStatusbar vsStatusBar; + + private bool disposed; private Guid? lastAnalysisId; private CancellableJobRunner reanalysisJob; private StatusBarReanalysisProgressHandler reanalysisProgressHandler; - - private bool disposed; + private IVsStatusbar vsStatusBar; internal IEnumerable ActiveTrackersForTesting => issueTrackers; @@ -94,7 +96,8 @@ internal TaggerProvider( IFileTracker fileTracker, IAnalyzer analyzer, ILogger logger, - IInitializationProcessorFactory initializationProcessorFactory) + IInitializationProcessorFactory initializationProcessorFactory, + ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory) { this.sonarErrorDataSource = sonarErrorDataSource; this.textDocumentFactoryService = textDocumentFactoryService; @@ -107,6 +110,7 @@ internal TaggerProvider( this.fileTracker = fileTracker; this.analyzer = analyzer; this.logger = logger; + this.taskExecutorWithDebounceFactory = taskExecutorWithDebounceFactory; InitializationProcessor = initializationProcessorFactory.CreateAndStart( [], @@ -117,7 +121,22 @@ internal TaggerProvider( })); } - public IInitializationProcessor InitializationProcessor { get; private set; } + public void Dispose() + { + if (disposed) + { + return; + } + + if (InitializationProcessor.IsFinalized) + { + analysisRequester.AnalysisRequested -= OnAnalysisRequested; + } + + disposed = true; + } + + public IInitializationProcessor InitializationProcessor { get; } private void OnAnalysisRequested(object sender, AnalysisRequestEventArgs args) { @@ -176,7 +195,7 @@ private void NotifyFileTracker(IEnumerable filteredIssueTrackers) #region IViewTaggerProvider members /// - /// Create a tagger that will track SonarLint issues on the view/buffer combination. + /// Create a tagger that will track SonarLint issues on the view/buffer combination. /// public ITagger CreateTagger(ITextBuffer buffer) where T : ITag { @@ -216,6 +235,7 @@ private TextBufferIssueTracker InternalCreateTextBufferIssueTracker(ITextDocumen vsProjectInfoProvider, issueConsumerFactory, issueConsumerStorage, + taskExecutorWithDebounceFactory.Create(debounceMilliseconds), logger); #endregion IViewTaggerProvider members @@ -279,19 +299,4 @@ public void OnDocumentClosed(IIssueTracker issueTracker) } #endregion IDocumentTracker methods - - public void Dispose() - { - if (disposed) - { - return; - } - - if (InitializationProcessor.IsFinalized) - { - analysisRequester.AnalysisRequested -= OnAnalysisRequested; - } - - disposed = true; - } } diff --git a/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs b/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs new file mode 100644 index 0000000000..260a69c005 --- /dev/null +++ b/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs @@ -0,0 +1,80 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core.Synchronization; + +namespace SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +internal interface ITaskExecutorWithDebounceFactory +{ + ITaskExecutorWithDebounce Create(TimeSpan debounceMilliseconds); +} + +internal interface ITaskExecutorWithDebounce +{ + Task DebounceAsync(Action task); +} + +[Export(typeof(ITaskExecutorWithDebounceFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class TaskExecutorWithDebounceFactory(IAsyncLockFactory asyncLockFactory) : ITaskExecutorWithDebounceFactory +{ + public ITaskExecutorWithDebounce Create(TimeSpan debounceMilliseconds) => new TaskExecutorWithDebounce(asyncLockFactory, debounceMilliseconds); +} + +internal class TaskExecutorWithDebounce(IAsyncLockFactory asyncLockFactory, TimeSpan debounceMilliseconds) : ITaskExecutorWithDebounce +{ + private sealed record Debounce(CancellationTokenSource CancellationTokenSource); + private Debounce latestDebounceState; + private readonly IAsyncLock asyncLock = asyncLockFactory.Create(); + + public async Task DebounceAsync(Action task) + { + Debounce latestState; + using (await asyncLock.AcquireAsync()) + { + latestDebounceState?.CancellationTokenSource.Cancel(); + latestDebounceState = new Debounce(new CancellationTokenSource()); + latestState = latestDebounceState; + } + + ExecuteAction(task, latestState); + } + + private void ExecuteAction(Action task, Debounce latestState) => + Task.Run(async () => + { + try + { + await Task.Delay(debounceMilliseconds, latestState.CancellationTokenSource.Token); + if (!latestState.CancellationTokenSource.Token.IsCancellationRequested) + { + task(); + } + } + catch (TaskCanceledException) + { + // do nothing + } + }, latestState.CancellationTokenSource.Token).Forget(); +} diff --git a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs index ab3d0a9fbc..66ac66cfbe 100644 --- a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs +++ b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs @@ -20,6 +20,7 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Integration.Vsix.Analysis; @@ -48,10 +49,12 @@ internal sealed class TextBufferIssueTracker : IIssueTracker, ITagger private readonly ITextDocument document; private readonly IIssueConsumerFactory issueConsumerFactory; private readonly IIssueConsumerStorage issueConsumerStorage; + private readonly ITaskExecutorWithDebounce taskExecutorWithDebounce; private readonly ILogger logger; private readonly ISonarErrorListDataSource sonarErrorDataSource; private readonly ITextBuffer textBuffer; private readonly IVsProjectInfoProvider vsProjectInfoProvider; + internal /* for testing */ TaggerProvider Provider { get; } internal /* for testing */ IssuesSnapshotFactory Factory { get; } @@ -63,6 +66,7 @@ public TextBufferIssueTracker( IVsProjectInfoProvider vsProjectInfoProvider, IIssueConsumerFactory issueConsumerFactory, IIssueConsumerStorage issueConsumerStorage, + ITaskExecutorWithDebounce taskExecutorWithDebounce, ILogger logger) { Provider = provider; @@ -72,6 +76,7 @@ public TextBufferIssueTracker( this.vsProjectInfoProvider = vsProjectInfoProvider; this.issueConsumerFactory = issueConsumerFactory; this.issueConsumerStorage = issueConsumerStorage; + this.taskExecutorWithDebounce = taskExecutorWithDebounce; this.logger = logger; logger.ForContext(nameof(TextBufferIssueTracker)); @@ -81,6 +86,10 @@ public TextBufferIssueTracker( Factory = new IssuesSnapshotFactory(LastAnalysisFilePath); document.FileActionOccurred += SafeOnFileActionOccurred; + if (textBuffer is ITextBuffer2 textBuffer2) + { + textBuffer2.ChangedOnBackground += TextBuffer_OnChangedOnBackground; + } sonarErrorDataSource.AddFactory(Factory); Provider.AddIssueTracker(this); @@ -91,18 +100,7 @@ public TextBufferIssueTracker( public string LastAnalysisFilePath { get; private set; } public IEnumerable DetectedLanguages { get; } - public void UpdateAnalysisState() - { - try - { - RemoveIssueConsumer(LastAnalysisFilePath); - InitializeAnalysisState(); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.WriteLine(Strings.Analysis_ErrorUpdatingAnalysisState, ex); - } - } + public void UpdateAnalysisState() => UpdateAnalysisState(null); public string GetText() => document.TextBuffer.CurrentSnapshot.GetText(); @@ -111,6 +109,10 @@ public void Dispose() RemoveIssueConsumer(LastAnalysisFilePath); document.FileActionOccurred -= SafeOnFileActionOccurred; textBuffer.Properties.RemoveProperty(TaggerProvider.SingletonManagerPropertyCollectionKey); + if (textBuffer is ITextBuffer2 textBuffer2) + { + textBuffer2.ChangedOnBackground -= TextBuffer_OnChangedOnBackground; + } sonarErrorDataSource.RemoveFactory(Factory); Provider.OnDocumentClosed(this); } @@ -151,6 +153,19 @@ private void SafeOnFileActionOccurred(object sender, TextDocumentFileActionEvent } } + private void UpdateAnalysisState(ITextSnapshot newTextSnapshot) + { + try + { + RemoveIssueConsumer(LastAnalysisFilePath); + InitializeAnalysisState(newTextSnapshot); + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(Strings.Analysis_ErrorUpdatingAnalysisState, ex); + } + } + private void SnapToNewSnapshot(IIssuesSnapshot newSnapshot) { // Tell our factory to snap to a new snapshot. @@ -159,11 +174,11 @@ private void SnapToNewSnapshot(IIssuesSnapshot newSnapshot) sonarErrorDataSource.RefreshErrorList(Factory); } - private AnalysisSnapshot GetAnalysisSnapshot() => new(LastAnalysisFilePath, document.TextBuffer.CurrentSnapshot); + private AnalysisSnapshot GetAnalysisSnapshot(ITextSnapshot newTextSnapshot = null) => new(LastAnalysisFilePath, newTextSnapshot ?? document.TextBuffer.CurrentSnapshot); - private void InitializeAnalysisState() + private void InitializeAnalysisState(ITextSnapshot newTextSnapshot = null) { - var analysisSnapshot = GetAnalysisSnapshot(); + var analysisSnapshot = GetAnalysisSnapshot(newTextSnapshot); CreateIssueConsumer(analysisSnapshot); } @@ -182,4 +197,12 @@ private static void ClearErrorList(string filePath, IIssueConsumer issueConsumer issueConsumer.SetIssues(filePath, []); issueConsumer.SetHotspots(filePath, []); } + + private void TextBuffer_OnChangedOnBackground(object sender, TextContentChangedEventArgs e) => + taskExecutorWithDebounce.DebounceAsync(() => + { + var textSnapshot = e.After; + UpdateAnalysisState(textSnapshot); + Provider.OnDocumentUpdated(document.FilePath, textSnapshot.GetText(), DetectedLanguages); + }).Forget(); } From 2c3a8ee555c26220a8cdf2789470e95e3a0c367b Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:37:56 +0200 Subject: [PATCH 26/38] SLVS-2427 Provide quickfixes (#6399) [SLVS-2427](https://sonarsource.atlassian.net/browse/SLVS-2427) [SLVS-2427]: https://sonarsource.atlassian.net/browse/SLVS-2427?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../Analysis/RoslynQuickFixTests.cs | 84 +++++ src/Core/Analysis/IQuickFix.cs | 5 + src/Core/Analysis/RoslynQuickFix.cs | 42 +++ src/EmbeddedSonarAnalyzer.props | 2 +- .../Integration.Vsix.UnitTests.csproj | 1 + .../RoslynQuickFixStorageTests.cs | 128 +++++++ .../packages.lock.json | 101 +++++ .../RoslynQuickFixApplication.cs | 37 ++ .../RoslynQuickFixes/RoslynQuickFixStorage.cs | 84 +++++ ...nalysisIssueVisualizationConverterTests.cs | 38 +- .../QuickFixActionsSourceProviderTests.cs | 2 + .../QuickFixes/QuickFixActionsSourceTests.cs | 1 + .../QuickFixSuggestedActionTests.cs | 77 +++- .../TextBasedQuickFixApplicationTests.cs | 8 +- .../QuickFixes/QuickFixActionsSource.cs | 5 +- .../QuickFixActionsSourceProvider.cs | 2 + .../QuickFixes/QuickFixSuggestedAction.cs | 89 +++-- .../AnalysisIssueVisualizationConverter.cs | 10 +- src/IssueViz/Models/IQuickFixApplication.cs | 2 +- src/IssueViz/Models/IRoslynQuickFixStorage.cs | 26 ++ .../Models/TextBasedQuickFixApplication.cs | 5 +- src/IssueViz/Resources.Designer.cs | 28 +- src/IssueViz/Resources.resx | 9 + ...oslynAnalysisConfigurationProviderTests.cs | 3 +- .../RoslynAnalyzerLoaderTests.cs | 2 +- .../DiagnosticToRoslynIssueConverterTests.cs | 28 +- .../Analysis/RoslynCodeActionFactoryTests.cs | 34 ++ .../RoslynProjectCompilationProviderTests.cs | 21 +- .../Analysis/RoslynQuickFixFactoryTests.cs | 154 ++++++++ .../SequentialRoslynAnalysisEngineTests.cs | 99 ++++- .../RoslynAnalysisServiceTests.cs | 3 +- .../RoslynQuickFixApplicationImplTests.cs | 123 ++++++ src/RoslynAnalyzerServer.UnitTests/app.config | 6 + .../IRoslynAnalysisConfigurationProvider.cs | 2 +- .../RoslynAnalysisConfigurationProvider.cs | 10 +- .../Configuration/RoslynAnalyzerLoader.cs | 2 +- ...cs => DiagnosticToRoslynIssueConverter.cs} | 7 +- .../IDiagnosticToRoslynIssueConverter.cs | 3 +- .../Analysis/IRoslynAnalysisEngine.cs | 2 +- .../Analysis/IRoslynCodeActionFactory.cs | 35 ++ .../IRoslynProjectCompilationProvider.cs | 2 +- .../Analysis/IRoslynQuickFixFactory.cs | 34 ++ .../Analysis/RoslynCodeActionFactory.cs | 43 +++ .../Analysis/RoslynIssue.cs | 9 +- .../RoslynProjectCompilationProvider.cs | 4 +- .../Analysis/RoslynQuickFixFactory.cs | 56 +++ .../SequentialRoslynAnalysisEngine.cs | 19 +- .../Wrappers/IRoslynCodeActionWrapper.cs | 31 ++ .../IRoslynCompilationWithAnalyzersWrapper.cs | 5 +- .../Wrappers/IRoslynCompilationWrapper.cs | 7 +- .../Wrappers/IRoslynDocumentWrapper.cs | 28 ++ .../Wrappers/IRoslynProjectWrapper.cs | 3 +- .../Wrappers/IRoslynSolutionWrapper.cs | 7 +- .../Wrappers/IRoslynWorkspaceWrapper.cs | 8 + .../Wrappers/RoslynCodeActionWrapper.cs | 33 ++ .../RoslynCompilationWithAnalyzersWrapper.cs | 5 +- .../Wrappers/RoslynCompilationWrapper.cs | 14 +- .../Wrappers/RoslynDocumentWrapper.cs | 30 ++ .../Analysis/Wrappers/RoslynProjectWrapper.cs | 5 +- .../Wrappers/RoslynSolutionWrapper.cs | 13 +- .../Wrappers/RoslynWorkspaceWrapper.cs | 9 +- .../ApplyChangesOperation.Roslyn.cs | 159 ++++++++ .../IRoslynQuicFixWriter.cs | 26 ++ .../InternalsVisibleTo.cs | 2 + .../Resources.Designer.cs | 20 +- src/RoslynAnalyzerServer/Resources.resx | 8 +- .../RoslynQuickFixApplicationImpl.cs | 55 +++ ...iseFindingToAnalysisIssueConverterTests.cs | 353 ++++++++---------- .../RaiseFindingToAnalysisIssueConverter.cs | 5 + 69 files changed, 1995 insertions(+), 318 deletions(-) create mode 100644 src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs create mode 100644 src/Core/Analysis/RoslynQuickFix.cs create mode 100644 src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs create mode 100644 src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs create mode 100644 src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs create mode 100644 src/IssueViz/Models/IRoslynQuickFixStorage.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/app.config rename src/RoslynAnalyzerServer/Analysis/{SonarRoslynDiagnosticsConverter.cs => DiagnosticToRoslynIssueConverter.cs} (92%) create mode 100644 src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs create mode 100644 src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs create mode 100644 src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs create mode 100644 src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs diff --git a/src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs b/src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs new file mode 100644 index 0000000000..300dfc3da3 --- /dev/null +++ b/src/Core.UnitTests/Analysis/RoslynQuickFixTests.cs @@ -0,0 +1,84 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Analysis; + +namespace SonarLint.VisualStudio.Core.UnitTests.Analysis; + +[TestClass] +public class RoslynQuickFixTests +{ + public static object[][] IdAndStorageValueTestData => + [ + [new Guid("64146AA8-AE93-4ABE-BD81-FCBE27FC4D3E"), "||64146aa8-ae93-4abe-bd81-fcbe27fc4d3e"], + [new Guid("6125AA53-4A10-4A07-84CD-9C4C758B26A3"), "||6125aa53-4a10-4a07-84cd-9c4c758b26a3"], + [new Guid("145A5840-0E94-41D1-8431-4BFA07F952C6"), "||145a5840-0e94-41d1-8431-4bfa07f952c6"] + ]; + + [TestMethod] + [DynamicData(nameof(IdAndStorageValueTestData))] + public void StorageValue_ContainsCorrectPrefix(Guid id, string expectedStorageValue) + { + var testSubject = new RoslynQuickFix(id); + + testSubject.Id.Should().Be(id); + testSubject.GetStorageValue().Should().Be(expectedStorageValue); + } + + [TestMethod] + [DynamicData(nameof(IdAndStorageValueTestData))] + public void TryParse_ValidInput_ReturnsTrue(Guid expectedId, string storageValue) + { + var result = RoslynQuickFix.TryParse(storageValue, out var quickFix); + + result.Should().BeTrue(); + quickFix.Should().NotBeNull(); + quickFix.Id.Should().Be(expectedId); + } + + [TestMethod] + [DataRow("")] + [DataRow(null)] + [DataRow("invalid")] + [DataRow("||")] + [DataRow("||not-a-guid")] + [DataRow("||BCF7C738-EEF5-4F41-A5FF-3D8DDC00540B")] + [DataRow("82D00A1A-3019-4E09-9895-BE42657DFB34")] + public void TryParse_InvalidInput_ReturnsFalse(string message) + { + var result = RoslynQuickFix.TryParse(message, out _); + + result.Should().BeFalse(); + } + + [TestMethod] + public void TryParse_RoundTrip_PreservesOriginalId() + { + var originalId = Guid.NewGuid(); + var originalQuickFix = new RoslynQuickFix(originalId); + var storageValue = originalQuickFix.GetStorageValue(); + + var success = RoslynQuickFix.TryParse(storageValue, out var parsedQuickFix); + + success.Should().BeTrue(); + parsedQuickFix.Id.Should().Be(originalId); + parsedQuickFix.GetStorageValue().Should().Be(storageValue); + } +} diff --git a/src/Core/Analysis/IQuickFix.cs b/src/Core/Analysis/IQuickFix.cs index 732aa878f6..3d7110e0a0 100644 --- a/src/Core/Analysis/IQuickFix.cs +++ b/src/Core/Analysis/IQuickFix.cs @@ -22,6 +22,11 @@ namespace SonarLint.VisualStudio.Core.Analysis { public interface IQuickFixBase; + public interface IRoslynQuickFix : IQuickFixBase + { + Guid Id { get; } + } + public interface ITextBasedQuickFix : IQuickFixBase { string Message { get; } diff --git a/src/Core/Analysis/RoslynQuickFix.cs b/src/Core/Analysis/RoslynQuickFix.cs new file mode 100644 index 0000000000..f5763a6ce8 --- /dev/null +++ b/src/Core/Analysis/RoslynQuickFix.cs @@ -0,0 +1,42 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Core.Analysis; + +public class RoslynQuickFix(Guid id) : IRoslynQuickFix +{ + private const string StoragePrefix = "||"; + + public Guid Id { get; } = id; + + public string GetStorageValue() => StoragePrefix + Id; + + public static bool TryParse(string message, out RoslynQuickFix o) + { + if (message is not null && message.StartsWith(StoragePrefix) && Guid.TryParse(message.Substring(StoragePrefix.Length), out var id)) + { + o = new RoslynQuickFix(id); + return true; + } + + o = null; + return false; + } +} diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 8a933ce868..e914faf7cd 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0.11 + 1.0.0.29 10.34.0.83431 1.0.0 diff --git a/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj b/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj index 2da37e19f3..3f8ca9d72b 100644 --- a/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj +++ b/src/Integration.Vsix.UnitTests/Integration.Vsix.UnitTests.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs b/src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs new file mode 100644 index 0000000000..0855fca3c9 --- /dev/null +++ b/src/Integration.Vsix.UnitTests/RoslynQuickFixes/RoslynQuickFixStorageTests.cs @@ -0,0 +1,128 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis.CodeActions; +using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Integration.Vsix.RoslynQuickFixes; +using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.Integration.UnitTests.RoslynQuickFixes; + +[TestClass] +public class RoslynQuickFixStorageTests +{ + private IActiveConfigScopeTracker configScopeTracker; + private RoslynQuickFixStorage testSubject; + + [TestInitialize] + public void TestInitialize() + { + configScopeTracker = Substitute.For(); + testSubject = new RoslynQuickFixStorage(configScopeTracker); + } + + [TestMethod] + public void MefCtor_CheckIsExported() + { + var requiredExports = new []{MefTestHelpers.CreateExport()}; + MefTestHelpers.CheckTypeCanBeImported(requiredExports); + MefTestHelpers.CheckTypeCanBeImported(requiredExports); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void Add_ThenTryGet_ReturnsTrueAndQuickFix() + { + var id = Guid.NewGuid(); + var quickFixImpl = CreateApplication(); + + testSubject.Add(id, quickFixImpl); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeTrue(); + retrievedQuickFix.Should().BeOfType().Which.Implementation.Should().BeSameAs(quickFixImpl); + } + + [TestMethod] + public void Add_ExistingId_OverwritesExistingValue() + { + var id = Guid.NewGuid(); + var originalQuickFixImpl = CreateApplication(); + var newQuickFixImpl = CreateApplication(); + + testSubject.Add(id, originalQuickFixImpl); + testSubject.Add(id, newQuickFixImpl); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeTrue(); + retrievedQuickFix.Should().BeOfType().Which.Implementation.Should().BeSameAs(newQuickFixImpl); + } + + [TestMethod] + public void TryGet_NonExistentId_ReturnsFalseAndNull() + { + var result = testSubject.TryGet(Guid.NewGuid(), out var retrievedQuickFix); + + result.Should().BeFalse(); + retrievedQuickFix.Should().BeNull(); + } + + [TestMethod] + public void ConfigScopeTracker_OnCurrentConfigurationScopeChanged_DefinitionChanged_ClearsCache() + { + var id = Guid.NewGuid(); + var quickFixImpl = CreateApplication(); + testSubject.Add(id, quickFixImpl); + + configScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith( + new ConfigurationScopeChangedEventArgs(definitionChanged: true)); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeFalse(); + retrievedQuickFix.Should().BeNull(); + } + + [TestMethod] + public void ConfigScopeTracker_OnCurrentConfigurationScopeChanged_DefinitionNotChanged_DoesNotClearCache() + { + var id = Guid.NewGuid(); + var quickFixImpl = CreateApplication(); + testSubject.Add(id, quickFixImpl); + + configScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith( + new ConfigurationScopeChangedEventArgs(definitionChanged: false)); + + var result = testSubject.TryGet(id, out var retrievedQuickFix); + + result.Should().BeTrue(); + retrievedQuickFix.Should().BeOfType().Which.Implementation.Should().BeSameAs(quickFixImpl); + } + + private static RoslynQuickFixApplicationImpl CreateApplication() => + new(Substitute.For(), Substitute.For(), Substitute.For()); +} diff --git a/src/Integration.Vsix.UnitTests/packages.lock.json b/src/Integration.Vsix.UnitTests/packages.lock.json index ad6c4a81a3..75045521bd 100644 --- a/src/Integration.Vsix.UnitTests/packages.lock.json +++ b/src/Integration.Vsix.UnitTests/packages.lock.json @@ -14,6 +14,19 @@ "resolved": "0.11.4", "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", + "dependencies": { + "Humanizer.Core": "2.2.0", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "[3.11.0]", + "System.Composition": "1.0.31", + "System.IO.Pipelines": "5.0.1" + } + }, "Microsoft.CSharp": { "type": "Direct", "requested": "[4.7.0, )", @@ -159,6 +172,11 @@ "resolved": "1.4.1", "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" + }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -200,6 +218,25 @@ "resolved": "16.5.0", "contentHash": "K0hfdWy+0p8DJXxzpNc4T5zHm4hf9QONAvyzvw3utKExmxRBShtV/+uHVYTblZWk+rIHNEHeglyXMmqfSshdFA==" }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.2", + "System.Collections.Immutable": "5.0.0", + "System.Memory": "4.5.4", + "System.Reflection.Metadata": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "16.6.1", @@ -1002,6 +1039,54 @@ "resolved": "4.5.0", "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" }, + "System.Composition": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Convention": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31", + "System.Composition.TypedParts": "1.0.31" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", + "dependencies": { + "System.Composition.Runtime": "1.0.31" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "1.0.31", + "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31" + } + }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "5.0.1", @@ -1081,6 +1166,14 @@ "resolved": "4.6.0", "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", + "dependencies": { + "System.Collections.Immutable": "5.0.0" + } + }, "System.Runtime": { "type": "Transitive", "resolved": "4.3.0", @@ -1134,6 +1227,14 @@ "resolved": "5.0.0", "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, "System.Threading.AccessControl": { "type": "Transitive", "resolved": "5.0.0", diff --git a/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs new file mode 100644 index 0000000000..aed20122d0 --- /dev/null +++ b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixApplication.cs @@ -0,0 +1,37 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Text; +using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer; + +namespace SonarLint.VisualStudio.Integration.Vsix.RoslynQuickFixes; + +public class RoslynQuickFixApplication(RoslynQuickFixApplicationImpl implementation) : IQuickFixApplication +{ + internal readonly RoslynQuickFixApplicationImpl Implementation = implementation; + + public string Message => Implementation.Message; + + public bool CanBeApplied(ITextSnapshot currentSnapshot) => true; + + public async Task ApplyAsync(ITextSnapshot currentSnapshot, CancellationToken cancellationToken) => + await Implementation.ApplyAsync(cancellationToken); +} diff --git a/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs new file mode 100644 index 0000000000..b422c21d7c --- /dev/null +++ b/src/Integration.Vsix/RoslynQuickFixes/RoslynQuickFixStorage.cs @@ -0,0 +1,84 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.IssueVisualization.Models; +using SonarLint.VisualStudio.RoslynAnalyzerServer; + +namespace SonarLint.VisualStudio.Integration.Vsix.RoslynQuickFixes; + +[Export(typeof(IRoslynQuickFixStorageWriter))] +[Export(typeof(IRoslynQuickFixProvider))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class RoslynQuickFixStorage : IRoslynQuickFixStorageWriter, IRoslynQuickFixProvider +{ + private readonly object locker = new(); + private readonly Dictionary cache = new(); + + [method: ImportingConstructor] + public RoslynQuickFixStorage(IActiveConfigScopeTracker configScopeTracker) + { + configScopeTracker.CurrentConfigurationScopeChanged += ConfigScopeTracker_OnCurrentConfigurationScopeChanged; // it's okay to miss initial events here, this is only used for cache cleanup + } + + private void ConfigScopeTracker_OnCurrentConfigurationScopeChanged(object sender, ConfigurationScopeChangedEventArgs e) + { + if (!e.DefinitionChanged) + { + return; + } + + ClearCache(); + } + + private void ClearCache() // todo https://sonarsource.atlassian.net/browse/SLVS-2540 clear cache on analysis for file + { + lock (locker) + { + cache.Clear(); + } + } + + public void Add( + Guid id, + RoslynQuickFixApplicationImpl impl) + { + lock (locker) + { + cache[id] = impl; + } + } + + public bool TryGet(Guid id, out IQuickFixApplication roslynQuickFix) + { + lock (locker) + { + if (cache.TryGetValue(id, out var quickFixImplementation)) + { + roslynQuickFix = new RoslynQuickFixApplication(quickFixImplementation); + return true; + } + } + + roslynQuickFix = null; + return false; + } +} diff --git a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs index ed0bed87e3..52c492f40b 100644 --- a/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs +++ b/src/IssueViz.UnitTests/AnalysisIssueVisualizationConverterTests.cs @@ -36,21 +36,24 @@ public class AnalysisIssueVisualizationConverterTests private ITextSnapshot textSnapshotMock; private AnalysisIssueVisualizationConverter testSubject; + private IRoslynQuickFixProvider roslynQuickFixProvider; [TestInitialize] public void TestInitialize() { issueSpanCalculatorMock = new Mock(); textSnapshotMock = Mock.Of(); + roslynQuickFixProvider = Substitute.For(); - testSubject = new AnalysisIssueVisualizationConverter(issueSpanCalculatorMock.Object, Substitute.For()); + testSubject = new AnalysisIssueVisualizationConverter(issueSpanCalculatorMock.Object, Substitute.For(), roslynQuickFixProvider); } [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); @@ -147,6 +150,37 @@ public void Convert_IssueHasQuickFixes_QuickFixesSpansAreCalculated() quickFix2.EditVisualizations[0].Span.Should().Be(span3); } + [TestMethod] + public void Convert_IssueHasRoslynQuickFix_RoslynQuickFixIsFound_AddsQuickFixToResult() + { + var roslynQuickFix = new RoslynQuickFix(Guid.NewGuid()); + var issue = CreateIssue(roslynQuickFix); + var expectedQuickFixApplication = Substitute.For(); + roslynQuickFixProvider.TryGet(roslynQuickFix.Id, out Arg.Any()) + .Returns(x => + { + x[1] = expectedQuickFixApplication; + return true; + }); + + var result = testSubject.Convert(issue, textSnapshotMock); + + result.QuickFixes.Should().BeEquivalentTo(expectedQuickFixApplication); + } + + [TestMethod] + public void Convert_IssueHasRoslynQuickFix_RoslynQuickFixNotFound_ReturnsIssueWithoutQuickFix() + { + var roslynQuickFix = new RoslynQuickFix(Guid.NewGuid()); + var issue = CreateIssue(roslynQuickFix); + roslynQuickFixProvider.TryGet(roslynQuickFix.Id, out Arg.Any()) + .Returns(false); + + var result = testSubject.Convert(issue, textSnapshotMock); + + result.QuickFixes.Should().BeEmpty(); + } + [TestMethod] public void Convert_EmptySpan_IssueWithEmptySpan() { diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs index 281445bada..7598458612 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProviderTests.cs @@ -43,6 +43,7 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -103,6 +104,7 @@ private static QuickFixActionsSourceProvider CreateTestSubject(ITextBuffer buffe bufferTagAggregatorFactoryService.Object, lightBulbBroker, Mock.Of(), + Substitute.For(), Mock.Of(), new NoOpThreadHandler()); } diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs index 7d7cf75abb..365e5c623a 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixActionsSourceTests.cs @@ -362,6 +362,7 @@ private QuickFixActionsSource CreateTestSubject( textView, textBuffer, Mock.Of(), + Substitute.For(), logger, threadHandling); } diff --git a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs index 0beff5619a..b732b0c9ae 100644 --- a/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs +++ b/src/IssueViz.UnitTests/Editor/QuickActions/QuickFixes/QuickFixSuggestedActionTests.cs @@ -18,7 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Windows; using Microsoft.VisualStudio.Text; +using NSubstitute.ExceptionExtensions; +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes; using SonarLint.VisualStudio.IssueVisualization.Models; @@ -38,6 +41,8 @@ public class QuickFixSuggestedActionTests private ITextSnapshot snapshot; private QuickFixSuggestedAction testSubject; private const string RuleId = "test-rule-id"; + private SnapshotSpan originalSpan; + private IMessageBox messageBox; [TestInitialize] public void TestInitialize() @@ -47,6 +52,12 @@ public void TestInitialize() textBuffer = CreateTextBuffer(snapshot); issueViz = Substitute.For(); issueViz.RuleId.Returns(RuleId); + messageBox = Substitute.For(); + + // Set up a non-empty span + originalSpan = new SnapshotSpan(snapshot, new Span(0, 10)); + issueViz.Span.Returns(originalSpan); + telemetryManager = Substitute.For(); logger = Substitute.ForPartsOf(); threadHandling = Substitute.ForPartsOf(); @@ -56,10 +67,14 @@ public void TestInitialize() textBuffer, issueViz, telemetryManager, + messageBox, logger, threadHandling); } + [TestMethod] + public void Ctor_SetsLogContext() => logger.Received(1).ForContext(Resources.QuickFixSuggestedAction_LogContext); + [TestMethod] public void DisplayName_ReturnsFixMessage() { @@ -72,7 +87,7 @@ public void DisplayName_ReturnsFixMessage() [TestMethod] public void Invoke_AppliesFixOnUiThreadWithTelemetry() { - ConfigureQuickFixApplication(); + ConfigureQuickFixApplicationCanBeApplied(true, true); testSubject.Invoke(CancellationToken.None); @@ -81,9 +96,34 @@ public void Invoke_AppliesFixOnUiThreadWithTelemetry() quickFixApplication.CanBeApplied(snapshot); threadHandling.Run(Arg.Any>>()); threadHandling.SwitchToMainThreadAsync(); - quickFixApplication.ApplyAsync(snapshot, issueViz, Arg.Any()); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()); telemetryManager.QuickFixApplied(RuleId); }); + issueViz.Received(1).Span = Arg.Is(s => s.IsEmpty); + issueViz.DidNotReceive().Span = originalSpan; + } + + [TestMethod] + public void Invoke_QuickFixApplicationReturnsFalse_SpanIsRestored_TelemetryNotSent() + { + ConfigureQuickFixApplicationCanBeApplied(true, false); + + testSubject.Invoke(CancellationToken.None); + + VerifyDidNotApply(); + } + + [TestMethod] + public void Invoke_QuickFixApplicationThrowsException_SpanIsRestored_TelemetryNotSent() + { + quickFixApplication.CanBeApplied(snapshot).Returns(true); + var exception = new Exception("test"); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()).ThrowsAsync(exception); + + var act = () => testSubject.Invoke(CancellationToken.None); + + act.Should().Throw().Which.Should().Be(exception); + VerifyDidNotApply(); } [TestMethod] @@ -91,7 +131,7 @@ public void Invoke_CancellationTokenIsCancelled_NoChanges() { testSubject.Invoke(new CancellationToken(canceled: true)); - quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default, default); + quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default); issueViz.DidNotReceiveWithAnyArgs().Span = Arg.Any(); telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); } @@ -99,16 +139,32 @@ public void Invoke_CancellationTokenIsCancelled_NoChanges() [TestMethod] public void Invoke_QuickFixIsNotApplicable_NoChanges() { - ConfigureNonApplicableQuickFixApplication(); + ConfigureQuickFixApplicationCanBeApplied(false, false); testSubject.Invoke(CancellationToken.None); quickFixApplication.Received(1).CanBeApplied(snapshot); - quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default, default); + quickFixApplication.DidNotReceiveWithAnyArgs().ApplyAsync(default, default); issueViz.DidNotReceiveWithAnyArgs().Span = Arg.Any(); telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); } + private void VerifyDidNotApply() + { + var didNotApplyMessage = string.Format(Resources.QuickFixSuggestedAction_CouldNotApply, RuleId); + Received.InOrder(() => + { + quickFixApplication.CanBeApplied(snapshot); + threadHandling.Run(Arg.Any>>()); + threadHandling.SwitchToMainThreadAsync(); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()); + messageBox.Show(didNotApplyMessage, Resources.QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption, MessageBoxButton.OK, MessageBoxImage.Error); + }); + issueViz.Received().Span = originalSpan; + telemetryManager.DidNotReceiveWithAnyArgs().QuickFixApplied(Arg.Any()); + logger.AssertPartialOutputStringExists(didNotApplyMessage); + } + private static ITextSnapshot CreateTextSnapshot() { var snapshot = Substitute.For(); @@ -116,14 +172,15 @@ private static ITextSnapshot CreateTextSnapshot() return snapshot; } - private void ConfigureQuickFixApplication() => ConfigureQuickFixApplication(true); - - private void ConfigureNonApplicableQuickFixApplication() => ConfigureQuickFixApplication(false); - - private void ConfigureQuickFixApplication(bool canBeApplied) => + private void ConfigureQuickFixApplicationCanBeApplied(bool canBeApplied, bool willBeApplied) + { quickFixApplication.CanBeApplied(snapshot) .Returns(canBeApplied); + quickFixApplication.ApplyAsync(snapshot, Arg.Any()) + .Returns(willBeApplied); + } + private static ITextBuffer CreateTextBuffer(ITextSnapshot snapShot) { var textBuffer = Substitute.For(); diff --git a/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs b/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs index 42abb5c439..51f5f7f498 100644 --- a/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs +++ b/src/IssueViz.UnitTests/Models/TextBasedQuickFixApplicationTests.cs @@ -33,7 +33,6 @@ public class TextBasedQuickFixApplicationTests private ISpanTranslator spanTranslator; private ITextBuffer textBuffer; private ITextEdit textEdit; - private IAnalysisIssueVisualization issueViz; private TextBasedQuickFixApplication testSubject; [TestInitialize] @@ -43,7 +42,6 @@ public void TestInitialize() snapshot.Length.Returns(int.MaxValue); quickFixVisualization = Substitute.For(); spanTranslator = Substitute.For(); - issueViz = Substitute.For(); testSubject = new TextBasedQuickFixApplication(quickFixVisualization, spanTranslator); @@ -83,14 +81,13 @@ public async Task ApplyAsync_CreatesEditAndAppliesAllChanges() (span1, "new text 1", translatedSpan1), (span2, "new text 2", translatedSpan2)); - await testSubject.ApplyAsync(snapshot, issueViz, CancellationToken.None); + await testSubject.ApplyAsync(snapshot, CancellationToken.None); textBuffer.Received(1).CreateEdit(); spanTranslator.Received(1).TranslateTo(span1, snapshot, SpanTrackingMode.EdgeExclusive); spanTranslator.Received(1).TranslateTo(span2, snapshot, SpanTrackingMode.EdgeExclusive); textEdit.Received(1).Replace(translatedSpan1.Span, "new text 1"); textEdit.Received(1).Replace(translatedSpan2.Span, "new text 2"); - issueViz.Received().Span = Arg.Is(x => x.Length == 0); textEdit.Received(1).Apply(); } @@ -101,12 +98,11 @@ public async Task ApplyAsync_CancellationRequested_DoesNotApplyChanges() var span = new SnapshotSpan(snapshot, new Span(1, 10)); SetupEditVisualizations((span, "new text", span)); - var act = () => testSubject.ApplyAsync(snapshot, issueViz, cancellationToken); + var act = () => testSubject.ApplyAsync(snapshot, cancellationToken); await act.Should().ThrowAsync(); textBuffer.Received(1).CreateEdit(); textEdit.DidNotReceiveWithAnyArgs().Apply(); - issueViz.DidNotReceiveWithAnyArgs().Span = default; } private void SetupTextBufferAndEdit() diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs index 72f36383bd..903471f81d 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSource.cs @@ -38,6 +38,7 @@ internal sealed class QuickFixActionsSource : ISuggestedActionsSource private readonly ILogger logger; private readonly IThreadHandling threadHandling; private readonly ITagAggregator issueLocationsTagAggregator; + private readonly IMessageBox messageBox; private readonly IQuickFixesTelemetryManager quickFixesTelemetryManager; public QuickFixActionsSource(ILightBulbBroker lightBulbBroker, @@ -45,6 +46,7 @@ public QuickFixActionsSource(ILightBulbBroker lightBulbBroker, ITextView textView, ITextBuffer textBuffer, IQuickFixesTelemetryManager quickFixesTelemetryManager, + IMessageBox messageBox, ILogger logger, IThreadHandling threadHandling) { @@ -52,6 +54,7 @@ public QuickFixActionsSource(ILightBulbBroker lightBulbBroker, this.textBuffer = textBuffer; this.textView = textView; this.quickFixesTelemetryManager = quickFixesTelemetryManager; + this.messageBox = messageBox; this.logger = logger; this.threadHandling = threadHandling; @@ -76,7 +79,7 @@ public IEnumerable GetSuggestedActions( { var applicableFixes = issueViz.QuickFixes.Where(x => x.CanBeApplied(textBuffer.CurrentSnapshot)); - allActions.AddRange(applicableFixes.Select(fix => new QuickFixSuggestedAction(fix, textBuffer, issueViz, quickFixesTelemetryManager, logger, threadHandling))); + allActions.AddRange(applicableFixes.Select(fix => new QuickFixSuggestedAction(fix, textBuffer, issueViz, quickFixesTelemetryManager, messageBox, logger, threadHandling))); } } } diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs index 1cee363d4d..f842413a9e 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixActionsSourceProvider.cs @@ -38,6 +38,7 @@ internal class QuickFixActionsSourceProvider( IBufferTagAggregatorFactoryService bufferTagAggregatorFactoryService, ILightBulbBroker lightBulbBroker, IQuickFixesTelemetryManager quickFixesTelemetryManager, + IMessageBox messageBox, ILogger logger, IThreadHandling threadHandling) : ISuggestedActionsSourceProvider @@ -62,6 +63,7 @@ public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, textView, textBuffer, quickFixesTelemetryManager, + messageBox, logger, threadHandling); } diff --git a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs index 9edc10656f..367356cd24 100644 --- a/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs +++ b/src/IssueViz/Editor/QuickActions/QuickFixes/QuickFixSuggestedAction.cs @@ -18,47 +18,86 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Windows; using Microsoft.VisualStudio.Text; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.IssueVisualization.Models; -namespace SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes +namespace SonarLint.VisualStudio.IssueVisualization.Editor.QuickActions.QuickFixes; + +internal class QuickFixSuggestedAction( + IQuickFixApplication quickFixApplication, + ITextBuffer textBuffer, + IAnalysisIssueVisualization issueViz, + IQuickFixesTelemetryManager quickFixesTelemetryManager, + IMessageBox messageBox, + ILogger logger, + IThreadHandling threadHandling) + : BaseSuggestedAction { - internal class QuickFixSuggestedAction( - IQuickFixApplication quickFixApplication, - ITextBuffer textBuffer, - IAnalysisIssueVisualization issueViz, - IQuickFixesTelemetryManager quickFixesTelemetryManager, - ILogger logger, - IThreadHandling threadHandling) - : BaseSuggestedAction + private readonly ILogger logger = logger.ForContext(Resources.QuickFixSuggestedAction_LogContext); + + public override string DisplayText => Resources.ProductNameCommandPrefix + quickFixApplication.Message; + + public override void Invoke(CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return; + } - public override string DisplayText => Resources.ProductNameCommandPrefix + quickFixApplication.Message; + if (!quickFixApplication.CanBeApplied(textBuffer.CurrentSnapshot)) + { + logger.LogVerbose("Quick fix cannot be applied as the text has changed. Issue: " + issueViz.RuleId); + return; + } - public override void Invoke(CancellationToken cancellationToken) + threadHandling.Run(async () => { - if (cancellationToken.IsCancellationRequested) - { - return; - } + await threadHandling.SwitchToMainThreadAsync(); + + var isHandled = await HandleQuickFixAsync(cancellationToken); - if (!quickFixApplication.CanBeApplied(textBuffer.CurrentSnapshot)) + if (isHandled) { - logger.LogVerbose("[Quick Fixes] Quick fix cannot be applied as the text has changed. Issue: " + issueViz.RuleId); - return; + quickFixesTelemetryManager.QuickFixApplied(issueViz.RuleId); } - threadHandling.Run(async () => - { - await threadHandling.SwitchToMainThreadAsync(); - await quickFixApplication.ApplyAsync(textBuffer.CurrentSnapshot, issueViz, cancellationToken); + return 0; + }); + } - quickFixesTelemetryManager.QuickFixApplied(issueViz.RuleId); + private async Task HandleQuickFixAsync(CancellationToken cancellationToken) + { + var originalSpan = issueViz.Span; + issueViz.InvalidateSpan(); + + var isApplied = false; - return 0; - }); + try + { + isApplied = await quickFixApplication.ApplyAsync(textBuffer.CurrentSnapshot, cancellationToken); + } + finally + { + if (!isApplied) + { + issueViz.Span = originalSpan; + NotifyUser(); + } } + + return isApplied; + } + + private void NotifyUser() + { + logger.WriteLine(Resources.QuickFixSuggestedAction_CouldNotApply, issueViz.RuleId); + messageBox.Show( + string.Format(Resources.QuickFixSuggestedAction_CouldNotApply,issueViz.RuleId), + Resources.QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption, + MessageBoxButton.OK, + MessageBoxImage.Error); } } diff --git a/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs b/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs index c188330613..ec2c8496a8 100644 --- a/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs +++ b/src/IssueViz/Models/AnalysisIssueVisualizationConverter.cs @@ -36,7 +36,7 @@ public interface IAnalysisIssueVisualizationConverter [Export(typeof(IAnalysisIssueVisualizationConverter))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] - internal class AnalysisIssueVisualizationConverter(IIssueSpanCalculator issueSpanCalculator, ISpanTranslator spanTranslator) : IAnalysisIssueVisualizationConverter + internal class AnalysisIssueVisualizationConverter(IIssueSpanCalculator issueSpanCalculator, ISpanTranslator spanTranslator, IRoslynQuickFixProvider roslynQuickFixProvider) : IAnalysisIssueVisualizationConverter { private static readonly IReadOnlyList EmptyConvertedFlows = []; private static readonly IReadOnlyList EmptyConvertedFixes = []; @@ -114,6 +114,14 @@ private IReadOnlyList GetQuickFixVisualizations(IAnalysisI .Fixes .Select(fix => { + if (fix is IRoslynQuickFix roslynQuickFix) + { + if (roslynQuickFixProvider.TryGet(roslynQuickFix.Id, out var roslynQuickFixApplication)) + { + return roslynQuickFixApplication; + } + Debug.Fail("Roslyn quick fix not found"); + } if (fix is ITextBasedQuickFix textBasedQuickFix) { return HandleTextBasedQuickFix(textSnapshot, textBasedQuickFix); diff --git a/src/IssueViz/Models/IQuickFixApplication.cs b/src/IssueViz/Models/IQuickFixApplication.cs index 6e18d8aed0..ecde522d68 100644 --- a/src/IssueViz/Models/IQuickFixApplication.cs +++ b/src/IssueViz/Models/IQuickFixApplication.cs @@ -26,5 +26,5 @@ public interface IQuickFixApplication { string Message { get; } bool CanBeApplied(ITextSnapshot currentSnapshot); - Task ApplyAsync(ITextSnapshot currentSnapshot, IAnalysisIssueVisualization issueViz, CancellationToken cancellationToken); + Task ApplyAsync(ITextSnapshot currentSnapshot, CancellationToken cancellationToken); } diff --git a/src/IssueViz/Models/IRoslynQuickFixStorage.cs b/src/IssueViz/Models/IRoslynQuickFixStorage.cs new file mode 100644 index 0000000000..6543ae2b70 --- /dev/null +++ b/src/IssueViz/Models/IRoslynQuickFixStorage.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.IssueVisualization.Models; + +public interface IRoslynQuickFixProvider +{ + bool TryGet(Guid id, out IQuickFixApplication roslynQuickFix); +} diff --git a/src/IssueViz/Models/TextBasedQuickFixApplication.cs b/src/IssueViz/Models/TextBasedQuickFixApplication.cs index 4fe9a0a203..a7156af2c8 100644 --- a/src/IssueViz/Models/TextBasedQuickFixApplication.cs +++ b/src/IssueViz/Models/TextBasedQuickFixApplication.cs @@ -31,7 +31,7 @@ internal class TextBasedQuickFixApplication(ITextBasedQuickFixVisualization text public bool CanBeApplied(ITextSnapshot currentSnapshot) => QuickFixVisualization.CanBeApplied(currentSnapshot); - public Task ApplyAsync(ITextSnapshot currentSnapshot, IAnalysisIssueVisualization issueViz, CancellationToken cancellationToken) + public Task ApplyAsync(ITextSnapshot currentSnapshot, CancellationToken cancellationToken) { var textBuffer = currentSnapshot.TextBuffer; var textEdit = textBuffer.CreateEdit(); @@ -45,8 +45,7 @@ public Task ApplyAsync(ITextSnapshot currentSnapshot, IAnalysisIssueVisualizatio cancellationToken.ThrowIfCancellationRequested(); - issueViz.InvalidateSpan(); textEdit.Apply(); - return Task.CompletedTask; + return Task.FromResult(true); } } diff --git a/src/IssueViz/Resources.Designer.cs b/src/IssueViz/Resources.Designer.cs index 4a1ac91eb0..eef4bf5ca2 100644 --- a/src/IssueViz/Resources.Designer.cs +++ b/src/IssueViz/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -204,6 +203,33 @@ public static string ProductNameCommandPrefix { } } + /// + /// Looks up a localized string similar to Could not apply Quick Fix for {0}. Please reanalyze the file and try again.. + /// + public static string QuickFixSuggestedAction_CouldNotApply { + get { + return ResourceManager.GetString("QuickFixSuggestedAction_CouldNotApply", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "SonarQube QuickFix". + /// + public static string QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption { + get { + return ResourceManager.GetString("QuickFixSuggestedAction_CouldNotApplyMessageBoxCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quick Fix. + /// + public static string QuickFixSuggestedAction_LogContext { + get { + return ResourceManager.GetString("QuickFixSuggestedAction_LogContext", resourceCulture); + } + } + /// /// Looks up a localized string similar to SonarQube: Show Issue Visualization ({0}). /// diff --git a/src/IssueViz/Resources.resx b/src/IssueViz/Resources.resx index 3fcc8a3bd5..290adbdd99 100644 --- a/src/IssueViz/Resources.resx +++ b/src/IssueViz/Resources.resx @@ -168,4 +168,13 @@ SonarQube: + + Could not apply Quick Fix for {0}. Please reanalyze the file and try again. + + + "SonarQube QuickFix" + + + Quick Fix + \ No newline at end of file diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs index 1308191f6f..f329aa6d22 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalysisConfigurationProviderTests.cs @@ -91,6 +91,7 @@ public void MefCtor_CheckIsExported() => [TestMethod] public void Ctor_SetsLogContext() => testLogger.Received(1).ForContext( + Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); @@ -313,7 +314,7 @@ public async Task GetConfigurationAsync_RunsOnBackgroundThread() { await testSubject.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto); - threadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>>()).IgnoreAwaitForAssert(); + threadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>>()).IgnoreAwaitForAssert(); } private Dictionary SetUpXmlConfigurations(Dictionary profiles) diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs index 00249f7964..3aeb13e25c 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Configuration/RoslynAnalyzerLoaderTests.cs @@ -41,6 +41,6 @@ public void Ctor_SetsLogContext() _ = new RoslynAnalyzerLoader(logger); - logger.Received().ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); + logger.Received().ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); } } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs index 726ce43c5f..a618bdeae0 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs @@ -22,6 +22,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.TestInfrastructure; @@ -78,7 +79,7 @@ public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( expectedRuleId, expectedLocation); - var result = testSubject.ConvertToSonarDiagnostic(diagnostic, language); + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], language); result.Should().BeEquivalentTo(expectedDiagnostic); } @@ -121,11 +122,34 @@ public void ConvertToSonarDiagnostic_WithSecondaryLocations_ConvertsCorrectly( }) }; - var result = testSubject.ConvertToSonarDiagnostic(diagnostic, Language.CSharp); + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], Language.CSharp); result.Flows.Should().BeEquivalentTo(expectedFlows); } + [TestMethod] + public void ConvertToSonarDiagnostic_WithQuickFixes_ConvertsCorrectly() + { + var diagnostic = CreateDiagnostic("any", "any", CreateLocation("any", 0, 0, 0, 0)); + var quickFix1 = new RoslynQuickFix(Guid.NewGuid()); + var quickFix2 = new RoslynQuickFix(Guid.NewGuid()); + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [quickFix1, quickFix2], Language.CSharp); + + result.QuickFixes.Should().BeEquivalentTo([new RoslynIssueQuickFix(quickFix1.GetStorageValue()), new RoslynIssueQuickFix(quickFix2.GetStorageValue())], + options => options.ComparingByMembers()); + } + + [TestMethod] + public void ConvertToSonarDiagnostic_WithNoQuickFixes_ReturnsEmptyQuickFixesList() + { + var diagnostic = CreateDiagnostic("any", "any", CreateLocation("any", 0, 0, 0, 0)); + + var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], Language.CSharp); + + result.QuickFixes.Should().BeEmpty(); + } + private static Location CreateLocation( string filePath, int startLine, diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs new file mode 100644 index 0000000000..328b16ce01 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynCodeActionFactoryTests.cs @@ -0,0 +1,34 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynCodeActionFactoryTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs index 1ddafad62d..45ba3d60cc 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs @@ -43,7 +43,7 @@ public class RoslynProjectCompilationProviderTests private IRoslynCompilationWrapper compilation = null!; private CompilationOptions compilationOptions = null!; private IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers = null!; - private ImmutableDictionary configurations = null!; + private ImmutableDictionary configurations = null!; private ImmutableDictionary diagnosticOptions = null!; private AdditionalText existingAdditionalFile = null!; private TestLogger logger = null!; @@ -63,12 +63,13 @@ public void TestInitialize() SetUpCodeFixProviders(); diagnosticOptions = ImmutableDictionary.Empty .Add("SomeId", ReportDiagnostic.Warn); - configurations = ImmutableDictionary.Empty + configurations = ImmutableDictionary.Empty .Add(Language.CSharp, new RoslynAnalysisConfiguration( sonarLintXml, diagnosticOptions, analyzers, codeFixProviders)); + SetUpCompilationWithAnalyzers(); testSubject = new RoslynProjectCompilationProvider(logger); } @@ -96,7 +97,8 @@ public async Task GetProjectCompilationAsync_ConfiguresCompilationWithCorrectOpt && options.Options.AdditionalFiles.SequenceEqual(ImmutableArray.Create(existingAdditionalFile, sonarLintXml), null as IEqualityComparer) && options.ConcurrentAnalysis == true && options.ReportSuppressedDiagnostics == false - && options.LogAnalyzerExecutionTime == false)); + && options.LogAnalyzerExecutionTime == false), + configurations[Language.CSharp]); } [TestMethod] @@ -107,7 +109,7 @@ public async Task GetProjectCompilationAsync_RemovesExistingSonarLintXml() var analyzerOptionsWithSonarLintXml = new AnalyzerOptions( ImmutableArray.Create(existingAdditionalFile, existingSonarLintXml)); project.RoslynAnalyzerOptions.Returns(analyzerOptionsWithSonarLintXml); - compilation.WithAnalyzers(Arg.Any>(), Arg.Any()) + compilation.WithAnalyzers(Arg.Any>(), Arg.Any(), configurations[Language.CSharp]) .Returns(compilationWithAnalyzers); await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); @@ -119,7 +121,8 @@ public async Task GetProjectCompilationAsync_RemovesExistingSonarLintXml() && options.Options.AdditionalFiles.SequenceEqual(ImmutableArray.Create(existingAdditionalFile, sonarLintXml), null as IEqualityComparer) && options.ConcurrentAnalysis == true && options.ReportSuppressedDiagnostics == false - && options.LogAnalyzerExecutionTime == false)); + && options.LogAnalyzerExecutionTime == false), + configurations[Language.CSharp]); } [TestMethod] @@ -128,7 +131,7 @@ public async Task GetProjectCompilationAsync_AnalyzerException_LogsError() CompilationWithAnalyzersOptions capturedOptions = null!; compilation.WithAnalyzers( Arg.Any>(), - Arg.Do(x => capturedOptions = x)) + Arg.Do(x => capturedOptions = x), configurations[Language.CSharp]) .Returns(compilationWithAnalyzers); await testSubject.GetProjectCompilationAsync(project, configurations, CancellationToken.None); capturedOptions.Should().NotBeNull(); @@ -180,7 +183,9 @@ private void SetUpCompilation() compilationWithAnalyzers = Substitute.For(); compilation.Language.Returns(Language.CSharp); compilation.WithOptions(Arg.Any()).Returns(compilation); - compilation.WithAnalyzers(Arg.Any>(), Arg.Any()) - .Returns(compilationWithAnalyzers); } + + private void SetUpCompilationWithAnalyzers() => + compilation.WithAnalyzers(Arg.Any>(), Arg.Any(), configurations[Language.CSharp]) + .Returns(compilationWithAnalyzers); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs new file mode 100644 index 0000000000..011d49a81a --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynQuickFixFactoryTests.cs @@ -0,0 +1,154 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.ReturnsExtensions; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; + +[TestClass] +public class RoslynQuickFixFactoryTests +{ + private IRoslynWorkspaceWrapper workspaceWrapper = null!; + private IRoslynCodeActionFactory roslynCodeActionFactory = null!; + private IRoslynQuickFixStorageWriter quickFixStorage = null!; + private IRoslynSolutionWrapper solution = null!; + private Diagnostic diagnostic = null!; + private IRoslynDocumentWrapper document = null!; + private IReadOnlyCollection codeFixProviders = null!; + private CancellationToken token; + private RoslynQuickFixFactory testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + codeFixProviders = [Substitute.For(), Substitute.For()]; + document = Substitute.For(); + diagnostic = CreateDiagnostic("rule1"); + workspaceWrapper = Substitute.For(); + roslynCodeActionFactory = Substitute.For(); + quickFixStorage = Substitute.For(); + solution = Substitute.For(); + token = CancellationToken.None; + + testSubject = new RoslynQuickFixFactory(workspaceWrapper, roslynCodeActionFactory, quickFixStorage); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task CreateQuickFixesAsync_NoCodeFixProvidersForRule_ReturnsEmptyList() + { + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, CreateAnalysisConfiguration([]), token); + + result.Should().BeEmpty(); + roslynCodeActionFactory.DidNotReceiveWithAnyArgs().GetCodeActionsAsync(default!, default!, default!, default).IgnoreAwaitForAssert(); + quickFixStorage.DidNotReceiveWithAnyArgs().Add(default, default!); + } + + [TestMethod] + public async Task CreateQuickFixesAsync_NoDocumentFound_ReturnsEmptyList() + { + var analysisConfiguration = CreateAnalysisConfiguration(new() { { diagnostic.Id, codeFixProviders } }); + solution.GetDocument(diagnostic.Location.SourceTree).ReturnsNull(); + + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, analysisConfiguration, token); + + result.Should().BeEmpty(); + roslynCodeActionFactory.DidNotReceiveWithAnyArgs().GetCodeActionsAsync(default!, default!, default!, default).IgnoreAwaitForAssert(); + quickFixStorage.DidNotReceiveWithAnyArgs().Add(default, default!); + } + + [TestMethod] + public async Task CreateQuickFixesAsync_NoCodeActionsFound_ReturnsEmptyList() + { + var analysisConfiguration = CreateAnalysisConfiguration(new() { { diagnostic.Id, codeFixProviders } }); + solution.GetDocument(diagnostic.Location.SourceTree).Returns(document); + roslynCodeActionFactory.GetCodeActionsAsync(default!, default!, default!, default) + .ReturnsForAnyArgs([]); + + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, analysisConfiguration, token); + + result.Should().BeEmpty(); + roslynCodeActionFactory.Received(1).GetCodeActionsAsync(codeFixProviders, diagnostic, document, token).IgnoreAwaitForAssert(); + quickFixStorage.DidNotReceiveWithAnyArgs().Add(default, default!); + } + + [TestMethod] + public async Task CreateQuickFixesAsync_WithCodeActions_ReturnsQuickFixesAndAddsToStorage() + { + var codeAction1 = Substitute.For(); + var codeAction2 = Substitute.For(); + var codeAction3 = Substitute.For(); + var analysisConfiguration = CreateAnalysisConfiguration(new() { { diagnostic.Id, codeFixProviders } }); + solution.GetDocument(diagnostic.Location.SourceTree).Returns(document); + roslynCodeActionFactory.GetCodeActionsAsync(Arg.Any>(), diagnostic, document, token) + .Returns([codeAction1, codeAction2, codeAction3]); + + var result = await testSubject.CreateQuickFixesAsync(diagnostic, solution, analysisConfiguration, token); + + result.Should().HaveCount(3); + roslynCodeActionFactory.Received(1).GetCodeActionsAsync(codeFixProviders, diagnostic, document, token).IgnoreAwaitForAssert(); + quickFixStorage.Received(1).Add(result[0].Id, Arg.Is(x => x.RoslynCodeAction == codeAction1)); + quickFixStorage.Received(1).Add(result[1].Id, Arg.Is(x => x.RoslynCodeAction == codeAction2)); + quickFixStorage.Received(1).Add(result[2].Id, Arg.Is(x => x.RoslynCodeAction == codeAction3)); + } + + private static Diagnostic CreateDiagnostic(string id) + { + var descriptor = new DiagnosticDescriptor( + id, + "any", + "any", + "any", + DiagnosticSeverity.Warning, + true); + + var location = Location.Create( + "any", + new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); + + return Diagnostic.Create(descriptor, location); + } + + private static RoslynAnalysisConfiguration CreateAnalysisConfiguration( + Dictionary> codeFixProvidersByRuleKey) => + new(null!, + null!, + default, + codeFixProvidersByRuleKey.ToImmutableDictionary()); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs index d5d8246f86..426bd08399 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs @@ -22,6 +22,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Integration.TestInfrastructure; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; @@ -36,9 +37,11 @@ public class SequentialRoslynAnalysisEngineTests private IDiagnosticToRoslynIssueConverter issueConverter = null!; private IRoslynProjectCompilationProvider projectCompilationProvider = null!; private TestLogger logger = null!; - private ImmutableDictionary configurations = null!; + private ImmutableDictionary configurations = null!; private CancellationToken cancellationToken; private SequentialRoslynAnalysisEngine testSubject = null!; + private IRoslynQuickFixFactory roslynQuickFixFactory = null!; + private IRoslynSolutionWrapper solution = null!; [TestInitialize] public void TestInitialize() @@ -46,10 +49,13 @@ public void TestInitialize() issueConverter = Substitute.For(); projectCompilationProvider = Substitute.For(); logger = Substitute.ForPartsOf(); + roslynQuickFixFactory = Substitute.For(); + roslynQuickFixFactory.CreateQuickFixesAsync(default!, default!, default, default).ReturnsForAnyArgs([]); + solution = Substitute.For(); - testSubject = new SequentialRoslynAnalysisEngine(issueConverter, projectCompilationProvider, logger); + testSubject = new SequentialRoslynAnalysisEngine(issueConverter, projectCompilationProvider, roslynQuickFixFactory, logger); - configurations = ImmutableDictionary.Create(); + configurations = ImmutableDictionary.Create(); cancellationToken = new CancellationToken(); } @@ -58,6 +64,7 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); [TestMethod] @@ -164,22 +171,65 @@ public async Task AnalyzeAsync_SingleProjectWithMultipleCommands_ReturnsAllDiagn VerifyAnalysisExecution(requestForProject, compilationForProject, [diagnostic1A, diagnostic1B, diagnostic2A, diagnostic2B]); } + [TestMethod] + public async Task AnalyzeAsync_WithMultipleDiagnostics_CreatesQuickFixesForEach() + { + var analysisConfiguration1 = new RoslynAnalysisConfiguration(); + var noQuickFixes = new List(); + var (diagnosticWith0Fixes, sonarIssueWith0Fixes) = SetupDiagnosticWithQuickFixes("rule1", "message1", noQuickFixes, analysisConfiguration1); + var oneQuickFix = new List { new(Guid.NewGuid()) }; + var (diagnosticWith1Fix, sonarIssueWith1Fix) = SetupDiagnosticWithQuickFixes("rule2", "message2", oneQuickFix, analysisConfiguration1); + + var project2Configuration = new RoslynAnalysisConfiguration(); + var twoQuickFixes = new List { new(Guid.NewGuid()), new(Guid.NewGuid()) }; + var (diagnostic2FixesProject2, sonarIssue2FixesProject2) = SetupDiagnosticWithQuickFixes("rule3", "message3", twoQuickFixes, project2Configuration); + + var (requestForProject1, compilationForProject1) = SetupProjectAnalysisRequestAndCompilation([[diagnosticWith0Fixes], [diagnosticWith1Fix]], analysisConfiguration1); + var (requestForProject2, compilationForProject2) = SetupProjectAnalysisRequestAndCompilation([[diagnostic2FixesProject2]], project2Configuration); + + var result = await testSubject.AnalyzeAsync([requestForProject1, requestForProject2], configurations, cancellationToken); + + result.Should().BeEquivalentTo([sonarIssueWith0Fixes, sonarIssueWith1Fix, sonarIssue2FixesProject2], options => options.Excluding(x => x.QuickFixes)); // factory tests will be used to verify the quickfixes + VerifyAnalysisExecution(requestForProject1, compilationForProject1, [(diagnosticWith0Fixes, noQuickFixes), (diagnosticWith1Fix, oneQuickFix)]); + VerifyAnalysisExecution(requestForProject2, compilationForProject2, [(diagnostic2FixesProject2, twoQuickFixes)]); + } + + private (Diagnostic diagnostic, RoslynIssue sonarIssue) SetupDiagnosticWithQuickFixes( + string ruleId, + string message, + List quickFixes, + RoslynAnalysisConfiguration analysisConfiguration) + { + var (diagnostic, sonarIssue) = SetUpDiagnosticAndConvertedModel(ruleId, message, null, quickFixes); + + roslynQuickFixFactory.CreateQuickFixesAsync( + diagnostic, + solution, + analysisConfiguration, + cancellationToken) + .Returns(quickFixes); + + return (diagnostic, sonarIssue); + } + private (RoslynProjectAnalysisRequest request, IRoslynCompilationWithAnalyzersWrapper compilation) SetupProjectAnalysisRequestAndCompilation( - Diagnostic[][] diagnosticsPerCommand) + Diagnostic[][] diagnosticsPerCommand, + RoslynAnalysisConfiguration? analysisConfiguration = null) { - var (project, projectCompilation) = SetupProjectAnalysisRequestAndCompilation(); - var analysisCommands = diagnosticsPerCommand.Select(x => SetupCommandWithDiagnostics(projectCompilation, x)).ToArray(); + var (project, projectCompilation) = SetupProjectAnalysisRequestAndCompilation(analysisConfiguration); + var commands = diagnosticsPerCommand.Select(x => SetupCommandWithDiagnostics(projectCompilation, x)).ToArray(); - return (new RoslynProjectAnalysisRequest(project, analysisCommands), projectCompilation); + return (new RoslynProjectAnalysisRequest(project, commands), projectCompilation); } - private RoslynProjectAnalysisRequest CreateProjectRequest(IRoslynProjectWrapper project, params IRoslynAnalysisCommand[] commands) => - new(project, commands); + private RoslynProjectAnalysisRequest CreateProjectRequest(IRoslynProjectWrapper project, params IRoslynAnalysisCommand[] commands) => new(project, commands); - private (IRoslynProjectWrapper project, IRoslynCompilationWithAnalyzersWrapper projectCompilation) SetupProjectAnalysisRequestAndCompilation() + private (IRoslynProjectWrapper project, IRoslynCompilationWithAnalyzersWrapper projectCompilation) SetupProjectAnalysisRequestAndCompilation( + RoslynAnalysisConfiguration? analysisConfiguration = null) { var project = Substitute.For(); - var compilation = SetupCompilation(project); + project.Solution.Returns(solution); + var compilation = SetupCompilation(project, analysisConfiguration ?? new RoslynAnalysisConfiguration()); return (project, compilation); } @@ -197,19 +247,23 @@ private IRoslynAnalysisCommand SetupCommandWithDiagnostics( private (Diagnostic, RoslynIssue) SetUpDiagnosticAndConvertedModel( string ruleId, string message, - RoslynIssue? existingSonarIssue = null) + RoslynIssue? existingSonarIssue = null, + List? roslynQuickFixes = null) { var diagnostic = CreateTestDiagnostic(ruleId, message); var sonarIssue = existingSonarIssue ?? CreateSonarIssue(ruleId, message); - issueConverter.ConvertToSonarDiagnostic(diagnostic, Arg.Any()).Returns(sonarIssue); + issueConverter.ConvertToSonarDiagnostic(diagnostic, roslynQuickFixes ?? Arg.Any>(), Arg.Any()).Returns(sonarIssue); return (diagnostic, sonarIssue); } - private IRoslynCompilationWithAnalyzersWrapper SetupCompilation(IRoslynProjectWrapper project) + private IRoslynCompilationWithAnalyzersWrapper SetupCompilation( + IRoslynProjectWrapper project, + RoslynAnalysisConfiguration analysisConfiguration) { var compilationWithAnalyzers = Substitute.For(); + compilationWithAnalyzers.AnalysisConfiguration.Returns(analysisConfiguration); projectCompilationProvider.GetProjectCompilationAsync(project, configurations, cancellationToken) .Returns(compilationWithAnalyzers); return compilationWithAnalyzers; @@ -219,8 +273,18 @@ private void VerifyAnalysisExecution( RoslynProjectAnalysisRequest projectRequest, IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, Diagnostic[] diagnostics, - Language? language = null) + Language? language = null) => + VerifyAnalysisExecution( + projectRequest, + compilationWithAnalyzers, + diagnostics.Select(x => (x, new List())).ToArray(), + language); + private void VerifyAnalysisExecution( + RoslynProjectAnalysisRequest projectRequest, + IRoslynCompilationWithAnalyzersWrapper compilationWithAnalyzers, + (Diagnostic diagnostic, List quickFixes)[] diagnostics, + Language? language = null) { projectCompilationProvider.Received(1) .GetProjectCompilationAsync(projectRequest.Project, configurations, cancellationToken).IgnoreAwaitForAssert(); @@ -228,9 +292,10 @@ private void VerifyAnalysisExecution( { analysisCommand.Received(1).ExecuteAsync(compilationWithAnalyzers, cancellationToken).IgnoreAwaitForAssert(); } - foreach (var diagnostic in diagnostics) + foreach (var (diagnostic, roslynQuickFixes) in diagnostics) { - issueConverter.Received(1).ConvertToSonarDiagnostic(diagnostic, language ?? Arg.Any()); + roslynQuickFixFactory.Received(1).CreateQuickFixesAsync(diagnostic, projectRequest.Project.Solution, compilationWithAnalyzers.AnalysisConfiguration, cancellationToken); + issueConverter.Received(1).ConvertToSonarDiagnostic(diagnostic, Arg.Is>(x => x.SequenceEqual(roslynQuickFixes)), language ?? Arg.Any()); } } diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs index 04442a3907..ec635c5533 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; @@ -33,7 +34,7 @@ public class RoslynAnalysisServiceTests { private static readonly List DefaultActiveRules = new() { new ActiveRuleDto("sample-rule-id", new Dictionary { { "paramKey", "paramValue" } }) }; private static readonly Dictionary DefaultAnalysisProperties = new() { { "sonar.cs.any", "any" } }; - private static readonly Dictionary DefaultAnalysisConfigurations = new() { { Language.CSharp, new RoslynAnalysisConfiguration() } }; + private static readonly Dictionary DefaultAnalysisConfigurations = new() { { Language.CSharp, new RoslynAnalysisConfiguration() } }; private static readonly List DefaultProjectAnalysisRequests = new() { new RoslynProjectAnalysisRequest(Substitute.For(), []) }; private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new RoslynIssueLocation("any", "any", new RoslynIssueTextRange(1, 1, 1, 1))) }; private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs new file mode 100644 index 0000000000..3b5319a8a4 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynQuickFixApplicationImplTests.cs @@ -0,0 +1,123 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using SonarLint.VisualStudio.Integration.TestInfrastructure; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests; + +[TestClass] +public class RoslynQuickFixApplicationImplTests +{ + private IRoslynWorkspaceWrapper workspace = null!; + private IRoslynSolutionWrapper originalSolution = null!; + private IRoslynCodeActionWrapper codeAction = null!; + private CancellationToken cancellationToken; + private RoslynQuickFixApplicationImpl testSubject = null!; + + [TestInitialize] + public void TestInitialize() + { + cancellationToken = new CancellationToken(); + workspace = Substitute.For(); + originalSolution = Substitute.For(); + codeAction = Substitute.For(); + + testSubject = new RoslynQuickFixApplicationImpl(workspace, originalSolution, codeAction); + } + + [TestMethod] + public void Message_ReturnsCodeActionTitle() + { + const string title = "Test title"; + codeAction.Title.Returns(title); + + testSubject.Message.Should().Be(title); + } + + [TestMethod] + public async Task ApplyAsync_NoApplyChangesOperation_DoesNotApplyChanges() + { + codeAction.GetOperationsAsync(cancellationToken).Returns(ImmutableArray.Create(Substitute.For())); + + var result = await testSubject.ApplyAsync(cancellationToken); + + VerifyNotAppliedChanges(result); + } + + [TestMethod] + public async Task ApplyAsync_MultipleOperationsWithApplyChangesOperation_DoesNotApplyChanges() + { + var operations = ImmutableArray.Create(new Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation(CreateDummySolution()), Substitute.For()); + codeAction.GetOperationsAsync(cancellationToken).Returns(operations); + + var result = await testSubject.ApplyAsync(cancellationToken); + + VerifyNotAppliedChanges(result); + } + + [TestMethod] + public async Task ApplyAsync_HasApplyChangesOperation_CallsWorkspaceApplyChanges() + { + var applyChangesOperation = new Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation(CreateDummySolution()); + var operations = ImmutableArray.Create(applyChangesOperation); + codeAction.GetOperationsAsync(cancellationToken).Returns(operations); + workspace.ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken).Returns(true); + + var result = await testSubject.ApplyAsync(cancellationToken); + + result.Should().BeTrue(); + await workspace.Received(1).ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken); + } + + [TestMethod] + public async Task ApplyAsync_WorkspaceApplyChangesFails_ReturnsFalse() + { + var applyChangesOperation = new Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation(CreateDummySolution()); + var operations = ImmutableArray.Create(applyChangesOperation); + codeAction.GetOperationsAsync(cancellationToken).Returns(operations); + workspace.ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken).Returns(false); + + var result = await testSubject.ApplyAsync(cancellationToken); + + result.Should().BeFalse(); + await workspace.Received(1).ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken); + } + + private void VerifyNotAppliedChanges(bool result) + { + result.Should().BeFalse(); + workspace.DidNotReceiveWithAnyArgs().ApplyOrMergeChangesAsync(default!, default!, default).IgnoreAwaitForAssert(); + } + + private Solution CreateDummySolution() + { + var adhocWorkspace = new AdhocWorkspace(); + + var slnInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, null, + []); + adhocWorkspace.AddSolution(slnInfo); + + return adhocWorkspace.CurrentSolution; + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/app.config b/src/RoslynAnalyzerServer.UnitTests/app.config new file mode 100644 index 0000000000..7f9314106a --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/app.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs index 701c17d2fc..9576934609 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/IRoslynAnalysisConfigurationProvider.cs @@ -25,7 +25,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; internal interface IRoslynAnalysisConfigurationProvider { - Task> GetConfigurationAsync( + Task> GetConfigurationAsync( List activeRules, Dictionary analysisProperties, AnalyzerInfoDto analyzerInfo); diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs index 407305e377..71012165e8 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalysisConfigurationProvider.cs @@ -38,10 +38,10 @@ internal class RoslynAnalysisConfigurationProvider( ILogger logger) : IRoslynAnalysisConfigurationProvider { private readonly IAsyncLock asyncLock = asyncLockFactory.Create(); - private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); private AnalysisConfigurationCache? cache; - public Task> GetConfigurationAsync( + public Task> GetConfigurationAsync( List activeRules, Dictionary analysisProperties, AnalyzerInfoDto analyzerInfo) => @@ -70,9 +70,9 @@ private void BuildConfigurations( BuildConfigurations(analysisProfilesByLanguage)); } - private IReadOnlyDictionary BuildConfigurations(Dictionary analysisProfilesByLanguage) + private IReadOnlyDictionary BuildConfigurations(Dictionary analysisProfilesByLanguage) { - var configurations = new Dictionary(); + var configurations = new Dictionary(); foreach (var analyzerAndLanguage in analysisProfilesByLanguage) { var language = analyzerAndLanguage.Key; @@ -103,5 +103,5 @@ private IReadOnlyDictionary BuildConfigur return configurations; } - private record struct AnalysisConfigurationCache(AnalysisConfigurationParametersCache Parameters, IReadOnlyDictionary Configurations); + private record struct AnalysisConfigurationCache(AnalysisConfigurationParametersCache Parameters, IReadOnlyDictionary Configurations); } diff --git a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs index c2f51f098e..ae41b75d01 100644 --- a/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs +++ b/src/RoslynAnalyzerServer/Analysis/Configuration/RoslynAnalyzerLoader.cs @@ -33,7 +33,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; [ExcludeFromCodeCoverage] internal class RoslynAnalyzerLoader(ILogger logger) : IRoslynAnalyzerLoader { - private readonly ILogger logger = logger.ForContext(Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerLoaderLogContext); public LoadedAnalyzerClasses LoadAnalyzerAssembly(string filePath) { diff --git a/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs similarity index 92% rename from src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs rename to src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs index 312a039557..e69adcd789 100644 --- a/src/RoslynAnalyzerServer/Analysis/SonarRoslynDiagnosticsConverter.cs +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; @@ -28,11 +29,11 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; [PartCreationPolicy(CreationPolicy.Shared)] public class DiagnosticToRoslynIssueConverter : IDiagnosticToRoslynIssueConverter { - public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language language) => - // todo SLVS-2427 quick fixes + public RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, List quickFixes, Language language) => new(SonarCompositeRuleId.GetFullErrorCode(language.RepoInfo.Key, diagnostic.Id), ConvertLocation(diagnostic.Location.GetMappedLineSpan(), diagnostic.GetMessage()), - ConvertSecondaryLocations(diagnostic)); + ConvertSecondaryLocations(diagnostic), + quickFixes.Select(x => new RoslynIssueQuickFix(x.GetStorageValue())).ToList()); private static IReadOnlyList ConvertSecondaryLocations(Diagnostic diagnostic) { diff --git a/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs b/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs index d9c5309c9a..be9db27cf6 100644 --- a/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs +++ b/src/RoslynAnalyzerServer/Analysis/IDiagnosticToRoslynIssueConverter.cs @@ -20,10 +20,11 @@ using Microsoft.CodeAnalysis; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; public interface IDiagnosticToRoslynIssueConverter { - RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, Language language); + RoslynIssue ConvertToSonarDiagnostic(Diagnostic diagnostic, List quickFixes, Language language); } diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs index 24c7e6cb17..41557dd2c3 100644 --- a/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynAnalysisEngine.cs @@ -26,6 +26,6 @@ internal interface IRoslynAnalysisEngine { Task> AnalyzeAsync( List projectsAnalysis, - IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token); } diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs new file mode 100644 index 0000000000..c264d812f6 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynCodeActionFactory.cs @@ -0,0 +1,35 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynCodeActionFactory +{ + Task> GetCodeActionsAsync( + IReadOnlyCollection codeFixProviders, + Diagnostic diagnostic, + IRoslynDocumentWrapper document, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs index fd7767488e..bd3887cb62 100644 --- a/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynProjectCompilationProvider.cs @@ -28,6 +28,6 @@ internal interface IRoslynProjectCompilationProvider { Task GetProjectCompilationAsync( IRoslynProjectWrapper project, - IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token); } diff --git a/src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs b/src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs new file mode 100644 index 0000000000..b6a3b2a01d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/IRoslynQuickFixFactory.cs @@ -0,0 +1,34 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +internal interface IRoslynQuickFixFactory +{ + Task> CreateQuickFixesAsync( + Diagnostic diagnostic, + IRoslynSolutionWrapper solution, + RoslynAnalysisConfiguration analysisConfiguration, + CancellationToken token); +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs b/src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs new file mode 100644 index 0000000000..15c4b006a4 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynCodeActionFactory.cs @@ -0,0 +1,43 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynCodeActionFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +[ExcludeFromCodeCoverage] +internal class RoslynCodeActionFactory : IRoslynCodeActionFactory +{ + public async Task> GetCodeActionsAsync(IReadOnlyCollection codeFixProviders, Diagnostic diagnostic, IRoslynDocumentWrapper document, CancellationToken token) + { + var codeActions = new List(); + foreach (var codeFixProvider in codeFixProviders) + { + await codeFixProvider.RegisterCodeFixesAsync(new CodeFixContext(document.RoslynDocument, diagnostic, (c, _) => codeActions.Add(new RoslynCodeActionWrapper(c)), token)); + } + return codeActions; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs index 8c22b2bd26..4133ab647e 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs @@ -23,13 +23,20 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; public class RoslynIssue( string ruleId, RoslynIssueLocation primaryLocation, - IReadOnlyList? flows = null) + IReadOnlyList? flows = null, + IReadOnlyList? quickFixes = null) { private static readonly IReadOnlyList EmptyFlows = []; public string RuleId { get; } = ruleId; public RoslynIssueLocation PrimaryLocation { get; } = primaryLocation ?? throw new ArgumentNullException(nameof(primaryLocation)); public IReadOnlyList Flows { get; } = flows ?? EmptyFlows; + public IReadOnlyList QuickFixes { get; } = quickFixes ?? []; +} + +public class RoslynIssueQuickFix(string value) +{ + public string Value { get; } = value; } public class RoslynIssueFlow(IReadOnlyList locations) diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs index 9cd199195c..7e31a4e9d8 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs @@ -37,7 +37,7 @@ internal class RoslynProjectCompilationProvider(ILogger logger) : IRoslynProject public async Task GetProjectCompilationAsync( IRoslynProjectWrapper project, - IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token) { var compilation = await project.GetCompilationAsync(token); @@ -69,7 +69,7 @@ private IRoslynCompilationWithAnalyzersWrapper ApplyAnalyzersAndAdditionalFile( false); return compilation - .WithAnalyzers(analysisConfigurationForLanguage.Analyzers, compilationWithAnalyzersOptions); + .WithAnalyzers(analysisConfigurationForLanguage.Analyzers, compilationWithAnalyzersOptions, analysisConfigurationForLanguage); } private static IRoslynCompilationWrapper ApplyDiagnosticOptions( diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs b/src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs new file mode 100644 index 0000000000..aba7b492d7 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/RoslynQuickFixFactory.cs @@ -0,0 +1,56 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; + +[Export(typeof(IRoslynQuickFixFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +[method:ImportingConstructor] +internal class RoslynQuickFixFactory(IRoslynWorkspaceWrapper workspace, IRoslynCodeActionFactory roslynCodeActionFactory, IRoslynQuickFixStorageWriter quickFixStorage) + : IRoslynQuickFixFactory +{ + public async Task> CreateQuickFixesAsync( + Diagnostic diagnostic, + IRoslynSolutionWrapper solution, + RoslynAnalysisConfiguration analysisConfiguration, + CancellationToken token) + { + var quickFixes = new List(); + + if (analysisConfiguration.CodeFixProvidersByRuleKey.TryGetValue(diagnostic.Id, out var availableCodeFixProviders) + && solution.GetDocument(diagnostic.Location.SourceTree) is {} document + && await roslynCodeActionFactory.GetCodeActionsAsync(availableCodeFixProviders, diagnostic, document, token) is {} codeActions) + { + foreach (var codeAction in codeActions) + { + var id = Guid.NewGuid(); + quickFixStorage.Add(id, new RoslynQuickFixApplicationImpl(workspace, solution, codeAction)); + quickFixes.Add(new RoslynQuickFix(id)); + } + } + + return quickFixes; + } +} diff --git a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs index 85b4301e08..7c9217f445 100644 --- a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Collections.Immutable; using System.ComponentModel.Composition; using System.IO; using SonarLint.VisualStudio.Core; @@ -31,13 +30,14 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; internal class SequentialRoslynAnalysisEngine( IDiagnosticToRoslynIssueConverter issueConverter, IRoslynProjectCompilationProvider projectCompilationProvider, + IRoslynQuickFixFactory quickFixFactory, ILogger logger) : IRoslynAnalysisEngine { private readonly ILogger logger = logger.ForContext("Roslyn Analysis", "Engine"); public async Task> AnalyzeAsync( List projectsAnalysis, - IReadOnlyDictionary sonarRoslynAnalysisConfigurations, + IReadOnlyDictionary sonarRoslynAnalysisConfigurations, CancellationToken token) { var uniqueDiagnostics = new HashSet(DiagnosticDuplicatesComparer.Instance); @@ -48,14 +48,21 @@ public async Task> AnalyzeAsync( // todo SLVS-2467 issue streaming foreach (var analysisCommand in projectAnalysisCommands.AnalysisCommands) { - var issues = await analysisCommand.ExecuteAsync(compilationWithAnalyzers, token); + var diagnostics = await analysisCommand.ExecuteAsync(compilationWithAnalyzers, token); - foreach (var diagnostic in issues.Select(d => issueConverter.ConvertToSonarDiagnostic(d, compilationWithAnalyzers.Language))) + foreach (var diagnostic in diagnostics) { + var quickFixes = await quickFixFactory.CreateQuickFixesAsync( + diagnostic, + projectAnalysisCommands.Project.Solution, + compilationWithAnalyzers.AnalysisConfiguration, + token); + + var roslynIssue = issueConverter.ConvertToSonarDiagnostic(diagnostic, quickFixes, compilationWithAnalyzers.Language); // todo SLVS-2468 improve issue merging - if (!uniqueDiagnostics.Add(diagnostic)) + if (!uniqueDiagnostics.Add(roslynIssue)) { - logger.LogVerbose("Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}", diagnostic.RuleId, Path.GetFileName(diagnostic.PrimaryLocation.FilePath), diagnostic.PrimaryLocation.TextRange.StartLine); + logger.LogVerbose("Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}", roslynIssue.RuleId, Path.GetFileName(roslynIssue.PrimaryLocation.FilePath), roslynIssue.PrimaryLocation.TextRange.StartLine); } } } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs new file mode 100644 index 0000000000..c5e3938696 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCodeActionWrapper.cs @@ -0,0 +1,31 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeActions; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynCodeActionWrapper +{ + string Title { get; } + + Task> GetOperationsAsync(CancellationToken cancellationToken); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs index 9a127ecbb6..3f15e480c9 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWithAnalyzersWrapper.cs @@ -24,9 +24,10 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -public interface IRoslynCompilationWithAnalyzersWrapper +internal interface IRoslynCompilationWithAnalyzersWrapper { - Language Language { get; } + RoslynLanguage Language { get; } + RoslynAnalysisConfiguration AnalysisConfiguration { get; } SyntaxTree? GetSyntaxTree(string filePath); SemanticModel? GetSemanticModel(string filePath); diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs index bb25ae9ad4..b3a2fc9d0f 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynCompilationWrapper.cs @@ -28,9 +28,12 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; internal interface IRoslynCompilationWrapper { CompilationOptions RoslynCompilationOptions { get; } - Language Language { get; } + RoslynLanguage Language { get; } IRoslynCompilationWrapper WithOptions(CompilationOptions withSpecificDiagnosticOptions); - IRoslynCompilationWithAnalyzersWrapper WithAnalyzers(ImmutableArray analyzers, CompilationWithAnalyzersOptions compilationWithAnalyzersOptions); + IRoslynCompilationWithAnalyzersWrapper WithAnalyzers( + ImmutableArray analyzers, + CompilationWithAnalyzersOptions compilationWithAnalyzersOptions, + RoslynAnalysisConfiguration analysisConfiguration); } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs new file mode 100644 index 0000000000..679bf891de --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynDocumentWrapper.cs @@ -0,0 +1,28 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +internal interface IRoslynDocumentWrapper +{ + Document RoslynDocument { get; } +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs index 794889eef1..db8866853b 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynProjectWrapper.cs @@ -26,10 +26,9 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; internal interface IRoslynProjectWrapper { string Name { get; } - bool SupportsCompilation { get; } - AnalyzerOptions RoslynAnalyzerOptions { get; } + IRoslynSolutionWrapper Solution { get; } bool ContainsDocument( string filePath, diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs index e97c9d63f0..ad5cdfb1c7 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynSolutionWrapper.cs @@ -18,9 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Microsoft.CodeAnalysis; + namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; internal interface IRoslynSolutionWrapper { - public IEnumerable Projects { get; } + IEnumerable Projects { get; } + Solution RoslynSolution { get; } + + IRoslynDocumentWrapper? GetDocument(SyntaxTree? tree); } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs index e4e061676d..efad6b1d3d 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs @@ -18,9 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; + namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; internal interface IRoslynWorkspaceWrapper { IRoslynSolutionWrapper GetCurrentSolution(); + + Task ApplyOrMergeChangesAsync( + IRoslynSolutionWrapper originalSolution, + Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation operation, + CancellationToken cancellationToken); } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs new file mode 100644 index 0000000000..5d07c8aeef --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCodeActionWrapper.cs @@ -0,0 +1,33 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] +internal class RoslynCodeActionWrapper(CodeAction codeAction) : IRoslynCodeActionWrapper +{ + public string Title => codeAction.Title; + + public Task> GetOperationsAsync(CancellationToken cancellationToken) => codeAction.GetOperationsAsync(cancellationToken); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs index 1f2579d792..ced955a43b 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWithAnalyzersWrapper.cs @@ -27,9 +27,10 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; [ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace -public class RoslynCompilationWithAnalyzersWrapper(CompilationWithAnalyzers compilation, Language language) : IRoslynCompilationWithAnalyzersWrapper +internal class RoslynCompilationWithAnalyzersWrapper(CompilationWithAnalyzers compilation, RoslynAnalysisConfiguration analysisConfiguration, RoslynLanguage language) : IRoslynCompilationWithAnalyzersWrapper { - public Language Language { get; } = language; + public RoslynLanguage Language { get; } = language; + public RoslynAnalysisConfiguration AnalysisConfiguration { get; } = analysisConfiguration; public SyntaxTree? GetSyntaxTree(string filePath) => compilation.Compilation.SyntaxTrees.SingleOrDefault(x => filePath.Equals(x.FilePath)); diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs index 9e20e767d3..b80ba9fde5 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynCompilationWrapper.cs @@ -23,6 +23,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using SonarLint.VisualStudio.Core; +using Languages = SonarLint.VisualStudio.Core.Language; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; @@ -30,16 +31,19 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; internal class RoslynCompilationWrapper(Compilation roslynCompilation) : IRoslynCompilationWrapper { public CompilationOptions RoslynCompilationOptions => roslynCompilation.Options; - public Language Language { get; } = roslynCompilation.Language switch + public RoslynLanguage Language { get; } = roslynCompilation.Language switch { - LanguageNames.CSharp => Language.CSharp, - LanguageNames.VisualBasic => Language.VBNET, + LanguageNames.CSharp => Languages.CSharp, + LanguageNames.VisualBasic => Languages.VBNET, _ => throw new ArgumentOutOfRangeException(nameof(roslynCompilation)), }; public IRoslynCompilationWrapper WithOptions(CompilationOptions withSpecificDiagnosticOptions) => new RoslynCompilationWrapper(roslynCompilation.WithOptions(withSpecificDiagnosticOptions)); - public IRoslynCompilationWithAnalyzersWrapper WithAnalyzers(ImmutableArray analyzers, CompilationWithAnalyzersOptions compilationWithAnalyzersOptions) => - new RoslynCompilationWithAnalyzersWrapper(roslynCompilation.WithAnalyzers(analyzers, compilationWithAnalyzersOptions), Language); + public IRoslynCompilationWithAnalyzersWrapper WithAnalyzers( + ImmutableArray analyzers, + CompilationWithAnalyzersOptions compilationWithAnalyzersOptions, + RoslynAnalysisConfiguration analysisConfiguration) => + new RoslynCompilationWithAnalyzersWrapper(roslynCompilation.WithAnalyzers(analyzers, compilationWithAnalyzersOptions), analysisConfiguration, Language); } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs new file mode 100644 index 0000000000..f92efea206 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynDocumentWrapper.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] +internal class RoslynDocumentWrapper(Document roslynDocument) : IRoslynDocumentWrapper +{ + public Document RoslynDocument { get; } = roslynDocument; +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs index 263a030cce..824d6cbf7d 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynProjectWrapper.cs @@ -25,11 +25,12 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; [ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace -internal class RoslynProjectWrapper(Project project) : IRoslynProjectWrapper +internal class RoslynProjectWrapper(Project project, IRoslynSolutionWrapper solution) : IRoslynProjectWrapper { public string Name => project.Name; public bool SupportsCompilation => project.SupportsCompilation; - public AnalyzerOptions RoslynAnalyzerOptions => project.AnalyzerOptions; + public IRoslynSolutionWrapper Solution => solution; + public AnalyzerOptions RoslynAnalyzerOptions => project.AnalyzerOptions; public async Task GetCompilationAsync(CancellationToken token) => new RoslynCompilationWrapper((await project.GetCompilationAsync(token))!); diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs index 97b500ec7d..68a003cc5b 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynSolutionWrapper.cs @@ -24,7 +24,16 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; [ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace -internal class RoslynSolutionWrapper(Solution workspaceCurrentSolution) : IRoslynSolutionWrapper +internal class RoslynSolutionWrapper : IRoslynSolutionWrapper { - public IEnumerable Projects { get; } = workspaceCurrentSolution.Projects.Select(x => new RoslynProjectWrapper(x)); + public RoslynSolutionWrapper(Solution solution) + { + RoslynSolution = solution; + Projects = solution.Projects.Select(x => new RoslynProjectWrapper(x, this)); + } + + public IEnumerable Projects { get; } + public Solution RoslynSolution { get; } + + public IRoslynDocumentWrapper? GetDocument(SyntaxTree? tree) => RoslynSolution.GetDocument(tree) is {} roslynDocument ? new RoslynDocumentWrapper(roslynDocument) : null; } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs index ff68f60714..968a5c421c 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs @@ -21,7 +21,9 @@ using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.VisualStudio.LanguageServices; +using SonarLint.VisualStudio.Core; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; @@ -29,7 +31,12 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; [Export(typeof(IRoslynWorkspaceWrapper))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class RoslynWorkspaceWrapper([Import(typeof(VisualStudioWorkspace))] Workspace workspace) : IRoslynWorkspaceWrapper +internal class RoslynWorkspaceWrapper([Import(typeof(VisualStudioWorkspace))] Workspace workspace, ILogger logger) : IRoslynWorkspaceWrapper { + private readonly ILogger quickFixApplicationLogger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynQuickFixLogContext); + public IRoslynSolutionWrapper GetCurrentSolution() => new RoslynSolutionWrapper(workspace.CurrentSolution); + + public Task ApplyOrMergeChangesAsync(IRoslynSolutionWrapper originalSolution, Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation operation, CancellationToken cancellationToken) => + ApplyChangesOperation.ApplyOrMergeChangesAsync(workspace, originalSolution.RoslynSolution, operation.ChangedSolution, quickFixApplicationLogger, cancellationToken); } diff --git a/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs new file mode 100644 index 0000000000..8934eab1d5 --- /dev/null +++ b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.Core; +using Document = Microsoft.CodeAnalysis.Document; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +// This class was adapted from https://github.com/dotnet/roslyn/blob/75e79dace86b274327a1afe479228d82a06051a4/src/Workspaces/Core/Portable/CodeActions/Operations/ApplyChangesOperation.cs#L46 +[ExcludeFromCodeCoverage] +public static class ApplyChangesOperation +{ + internal static async Task ApplyOrMergeChangesAsync( + Workspace workspace, + Solution originalSolution, + Solution changedSolution, + ILogger logger, + CancellationToken cancellationToken) + { + var currentSolution = workspace.CurrentSolution; + + // if there was no intermediary edit, just apply the change fully. + if (workspace.TryApplyChanges(changedSolution)) + { + return true; + } + + // Otherwise, we need to see what changes were actually made and see if we can apply them. The general rules are: + // + // 1. we only support text changes when doing merges. Any other changes to projects/documents are not + // supported because it's very unclear what impact they may have wrt other workspace updates that have + // already happened. + // + // 2. For text changes, we only support it if the current text of the document we're changing itself has not + // changed. This means we can merge in edits if there were changes to unrelated files, but not if there + // are changes to the current file. + + var solutionChanges = changedSolution.GetChanges(originalSolution); + + if (SolutionChangedCritically(solutionChanges)) + { + logger.LogVerbose("Solution projects have changed, update no longer valid"); + return false; + // todo https://sonarsource.atlassian.net/browse/SLVS-2513 this will lead to invalid quickfixes if project configuration changes. + // do we need to reanalyze open files on major workspace changes? can modified analyzer references be ignored? + } + + // Take the actual current solution the workspace is pointing to and fork it with just the text changes the + // code action wanted to make. Then apply that fork back into the workspace. + var forkedSolution = currentSolution; + + foreach (var changedProject in solutionChanges.GetProjectChanges()) + { + // We only support text changes. If we see any other changes to this project, bail out immediately. + if (ProjectChangedCritically(changedProject)) + { + logger.LogVerbose("Project {0} has changed, update no longer valid", changedProject.NewProject.Name); + return false; + } + + // We have to at least have some changed document + var changedDocuments = GetChangedDocuments(changedProject); + + if (changedDocuments.Length == 0) + { + return false; + } + + foreach (var documentId in changedDocuments) + { + if (!GetDocuments(changedProject, documentId, out var originalDocument, out var changedDocument)) + { + return false; + } + + // it has to be a text change the operation wants to make. If the operation is making some other + // sort of change, we can't merge this operation in. + if (await changedDocument.GetTextVersionAsync(cancellationToken) == await originalDocument.GetTextVersionAsync(cancellationToken)) + { + return false; + } + + // If the document has gone away, we definitely cannot apply a text change to it. + var currentDocument = currentSolution.GetDocument(documentId); + if (currentDocument is null) + { + return false; + } + + // If the file contents changed in the current workspace, then we can't apply this change to it. + if (await originalDocument.GetTextVersionAsync(cancellationToken) != await currentDocument.GetTextVersionAsync(cancellationToken)) + { + return false; + } + + forkedSolution = forkedSolution.WithDocumentText(documentId, await changedDocument.GetTextAsync(cancellationToken)); + } + } + + return workspace.TryApplyChanges(forkedSolution); + } + + private static ImmutableArray GetChangedDocuments(ProjectChanges changedProject) + { + var changedDocuments = changedProject.GetChangedDocuments() + .Concat(changedProject.GetChangedAdditionalDocuments()) + .Concat(changedProject.GetChangedAnalyzerConfigDocuments()).ToImmutableArray(); + return changedDocuments; + } + + private static bool GetDocuments( + ProjectChanges changedProject, + DocumentId documentId, + [NotNullWhen(true)]out Document? originalDocument, + [NotNullWhen(true)]out Document? changedDocument) + { + originalDocument = changedProject.OldProject.Solution.GetDocument(documentId); + + if (originalDocument == null) + { + Debug.Fail("Original document not found"); + changedDocument = null; + return false; + } + + changedDocument = changedProject.NewProject.Solution.GetDocument(documentId); + + if (changedDocument == null) + { + Debug.Fail("Changed document not found"); + return false; + } + return true; + } + + private static bool SolutionChangedCritically(SolutionChanges solutionChanges) => + solutionChanges.GetAddedProjects().Any() || + solutionChanges.GetAddedAnalyzerReferences().Any() || + solutionChanges.GetRemovedProjects().Any() || + solutionChanges.GetRemovedAnalyzerReferences().Any(); + + private static bool ProjectChangedCritically(ProjectChanges changedProject) => + changedProject.GetAddedAdditionalDocuments().Any() || + changedProject.GetAddedAnalyzerConfigDocuments().Any() || + changedProject.GetAddedAnalyzerReferences().Any() || + changedProject.GetAddedDocuments().Any() || + changedProject.GetAddedMetadataReferences().Any() || + changedProject.GetAddedProjectReferences().Any() || + changedProject.GetRemovedAdditionalDocuments().Any() || + changedProject.GetRemovedAnalyzerConfigDocuments().Any() || + changedProject.GetRemovedAnalyzerReferences().Any() || + changedProject.GetRemovedDocuments().Any() || + changedProject.GetRemovedMetadataReferences().Any() || + changedProject.GetRemovedProjectReferences().Any(); +} diff --git a/src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs b/src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs new file mode 100644 index 0000000000..8737bce03d --- /dev/null +++ b/src/RoslynAnalyzerServer/IRoslynQuicFixWriter.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +public interface IRoslynQuickFixStorageWriter +{ + void Add(Guid id, RoslynQuickFixApplicationImpl impl); +} diff --git a/src/RoslynAnalyzerServer/InternalsVisibleTo.cs b/src/RoslynAnalyzerServer/InternalsVisibleTo.cs index 76ec7de5a9..88513ed7d1 100644 --- a/src/RoslynAnalyzerServer/InternalsVisibleTo.cs +++ b/src/RoslynAnalyzerServer/InternalsVisibleTo.cs @@ -23,6 +23,7 @@ #if SignAssembly [assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] [assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Integration.Vsix.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] // Moq -- see https://github.com/Moq/moq4/wiki/Quickstart [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] @@ -30,6 +31,7 @@ #else [assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests")] [assembly: InternalsVisibleTo("SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests")] +[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Integration.Vsix.UnitTests")] // Moq [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs index ee2edd4071..b5bd971783 100644 --- a/src/RoslynAnalyzerServer/Resources.Designer.cs +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -231,12 +231,30 @@ internal static string RoslynAnalysisConfigurationNoAnalyzers { } /// - /// Looks up a localized string similar to Roslyn Analysis. + /// Looks up a localized string similar to Analysis. /// internal static string RoslynAnalysisLogContext { get { return ResourceManager.GetString("RoslynAnalysisLogContext", resourceCulture); } } + + /// + /// Looks up a localized string similar to Roslyn. + /// + internal static string RoslynLogContext { + get { + return ResourceManager.GetString("RoslynLogContext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to QuickFix. + /// + internal static string RoslynQuickFixLogContext { + get { + return ResourceManager.GetString("RoslynQuickFixLogContext", resourceCulture); + } + } } } diff --git a/src/RoslynAnalyzerServer/Resources.resx b/src/RoslynAnalyzerServer/Resources.resx index 41bce7f135..218cd6936e 100644 --- a/src/RoslynAnalyzerServer/Resources.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -156,8 +156,11 @@ Location {0} + + Roslyn + - Roslyn Analysis + Analysis Configuration @@ -177,4 +180,7 @@ Failed to load class {0} from {1}: {2} + + QuickFix + \ No newline at end of file diff --git a/src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs b/src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs new file mode 100644 index 0000000000..64467955c4 --- /dev/null +++ b/src/RoslynAnalyzerServer/RoslynQuickFixApplicationImpl.cs @@ -0,0 +1,55 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer; + +public class RoslynQuickFixApplicationImpl +{ + private readonly IRoslynWorkspaceWrapper workspace; + private readonly IRoslynSolutionWrapper originalSolution; + internal readonly IRoslynCodeActionWrapper RoslynCodeAction; + + internal RoslynQuickFixApplicationImpl(IRoslynWorkspaceWrapper workspace, IRoslynSolutionWrapper originalSolution, IRoslynCodeActionWrapper roslynCodeAction) + { + this.workspace = workspace; + this.originalSolution = originalSolution; + RoslynCodeAction = roslynCodeAction; + } + + public string Message => RoslynCodeAction.Title; + + public async Task ApplyAsync(CancellationToken cancellationToken) + { + var codeActionOperations = await RoslynCodeAction.GetOperationsAsync(cancellationToken); + + var applyChangesOperation = codeActionOperations.FirstOrDefault(x => x is Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation) as Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation; + + // we're only interested in ApplyChangesOperation and there can only be one at a time: https://github.com/dotnet/roslyn/blob/75e79dace86b274327a1afe479228d82a06051a4/src/Workspaces/Core/Portable/CodeActions/Operations/ApplyChangesOperation.cs#L18 + if (applyChangesOperation == null || codeActionOperations.Length > 1) + { + Debug.Fail($"Unexpected quickfix result: {applyChangesOperation} out of {codeActionOperations.Length}"); + return false; + } + + return await workspace.ApplyOrMergeChangesAsync(originalSolution, applyChangesOperation, cancellationToken); + } +} diff --git a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs index d96aa5cab1..ecbde8cd1a 100644 --- a/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs +++ b/src/SLCore.UnitTests/Listener/Analysis/RaiseFindingToAnalysisIssueConverterTests.cs @@ -24,6 +24,7 @@ using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Listener.Analysis; using SonarLint.VisualStudio.SLCore.Listener.Analysis.Models; +using SonarLint.VisualStudio.SLCore.Protocol; using SonarLint.VisualStudio.SLCore.Service.Rules.Models; using CleanCodeAttribute = SonarLint.VisualStudio.SLCore.Common.Models.CleanCodeAttribute; using SoftwareQuality = SonarLint.VisualStudio.SLCore.Common.Models.SoftwareQuality; @@ -68,31 +69,33 @@ public void GetAnalysisIssues_HasNoIssues_ReturnsEmpty() public void GetAnalysisIssues_HasIssues_ConvertsCorrectly() { var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedIssueDto( - IssueWithFlowsAndQuickFixesUseCase.Issue1Id, - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - dateTimeOffset, - true, - false, - new TextRangeDto(1, 2, 3, 4), - null, - null, - "context1", - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); - var issue2 = new RaisedIssueDto(IssueWithFlowsAndQuickFixesUseCase.Issue2Id, - "serverKey2", - "ruleKey2", - "PrimaryMessage2", - dateTimeOffset, - true, - false, - new TextRangeDto(61, 62, 63, 64), - [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], - [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], - "context2", - new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); + var issue1 = CreateRaisedIssueDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + serverKey: "serverKey1", + ruleKey: "ruleKey1", + primaryMessage: "PrimaryMessage1", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(1, 2, 3, 4), + flows: null, + quickFixes: null, + ruleDescriptionContextKey: "context1", + severityMode: new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + + var issue2 = CreateRaisedIssueDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue2Id, + serverKey: "serverKey2", + ruleKey: "ruleKey2", + primaryMessage: "PrimaryMessage2", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(61, 62, 63, 64), + flows: [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], + quickFixes: [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], + ruleDescriptionContextKey: "context2", + severityMode: new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); var result = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1, issue2 }).ToList(); @@ -103,33 +106,37 @@ public void GetAnalysisIssues_HasIssues_ConvertsCorrectly() public void GetAnalysisIssues_HasHotspot_ConvertsCorrectly() { var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue1Id, - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - dateTimeOffset, - true, - false, - new TextRangeDto(1, 2, 3, 4), - null, - null, - "context1", - VulnerabilityProbability.HIGH, - HotspotStatus.FIXED, - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); - var issue2 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue2Id, - "serverKey2", - "ruleKey2", - "PrimaryMessage2", - dateTimeOffset, - true, - false, - new TextRangeDto(61, 62, 63, 64), - [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], - [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], - "context2", VulnerabilityProbability.HIGH, - HotspotStatus.FIXED, - new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); + var issue1 = CreateRaisedHotspotDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + serverKey: "serverKey1", + ruleKey: "ruleKey1", + primaryMessage: "PrimaryMessage1", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(1, 2, 3, 4), + flows: null, + quickFixes: null, + ruleDescriptionContextKey: "context1", + vulnerabilityProbability: VulnerabilityProbability.HIGH, + status: HotspotStatus.FIXED, + severityMode: new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + + var issue2 = CreateRaisedHotspotDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue2Id, + serverKey: "serverKey2", + ruleKey: "ruleKey2", + primaryMessage: "PrimaryMessage2", + introductionDate: dateTimeOffset, + isOnNewCode: true, + resolved: false, + textRange: new TextRangeDto(61, 62, 63, 64), + flows: [IssueWithFlowsAndQuickFixesUseCase.Issue2Flow1, IssueWithFlowsAndQuickFixesUseCase.Issue2Flow2], + quickFixes: [IssueWithFlowsAndQuickFixesUseCase.Issue2Fix1, IssueWithFlowsAndQuickFixesUseCase.Issue2Fix2], + ruleDescriptionContextKey: "context2", + vulnerabilityProbability: VulnerabilityProbability.HIGH, + status: HotspotStatus.FIXED, + severityMode: new MQRModeDetails(CleanCodeAttribute.COMPLETE, IssueWithFlowsAndQuickFixesUseCase.Issue2Impacts)); var result = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1, issue2 }).ToList(); @@ -141,21 +148,7 @@ public void GetAnalysisIssues_IssueHasUnflattenedFlows_FlattensIntoSingleFlow() { var analysisIssues = testSubject.GetAnalysisIssues(UnflattenedFlowsUseCase.FileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - UnflattenedFlowsUseCase.UnflattenedFlows, - default, - default, - new StandardModeDetails(default, default)) + CreateRaisedIssueDto(flows: UnflattenedFlowsUseCase.UnflattenedFlows) }); UnflattenedFlowsUseCase.VerifyFlattenedFlow(analysisIssues); @@ -166,23 +159,7 @@ public void GetAnalysisIssues_HotspotHasUnflattenedFlows_FlattensIntoSingleFlow( { var analysisIssues = testSubject.GetAnalysisIssues(UnflattenedFlowsUseCase.FileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - UnflattenedFlowsUseCase.UnflattenedFlows, - default, - default, - VulnerabilityProbability.HIGH, - HotspotStatus.SAFE, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto(flows: UnflattenedFlowsUseCase.UnflattenedFlows) }); UnflattenedFlowsUseCase.VerifyFlattenedFlow(analysisIssues); @@ -198,23 +175,8 @@ public void GetAnalysisIssues_HotspotHasVulnerabilityProbability_AnalysisHotspot { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - vulnerabilityProbability, - HotspotStatus.SAFE, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto( + vulnerabilityProbability: vulnerabilityProbability) }); analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().Be(expectedHotspotPriority); @@ -225,23 +187,7 @@ public void GetAnalysisIssues_HotspotHasNoVulnerabilityProbability_AnalysisHotsp { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - null, - HotspotStatus.SAFE, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto(vulnerabilityProbability: null) }); analysisIssues.Single().Should().BeOfType().Which.HotspotPriority.Should().BeNull(); @@ -257,23 +203,8 @@ public void GetAnalysisIssues_HotspotWithTwoHighImpactsForDifferentQualities_Get { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - null, - HotspotStatus.SAFE, - new MQRModeDetails(default, + CreateRaisedHotspotDto( + severityMode: new MQRModeDetails(default, [ new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.INFO), new ImpactDto(SoftwareQuality.RELIABILITY, severity), @@ -298,21 +229,8 @@ public void GetAnalysisIssues_IssueWithTwoHighImpactsForDifferentQualities_GetsT { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - new MQRModeDetails(default, + CreateRaisedIssueDto( + severityMode: new MQRModeDetails(default, [ new ImpactDto(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.INFO), new ImpactDto(SoftwareQuality.RELIABILITY, severity), @@ -333,49 +251,32 @@ public void GetAnalysisIssues_IssueWithTwoHighImpactsForDifferentQualities_GetsT [TestMethod] public void GetAnalysisIssues_TextRangeDtoIsNull_ConvertsCorrectly() { - var dateTimeOffset = DateTimeOffset.Now; - var issue1 = new RaisedHotspotDto(IssueWithFlowsAndQuickFixesUseCase.Issue1Id, - "serverKey1", - "ruleKey1", - "PrimaryMessage1", - dateTimeOffset, - true, - false, - textRange: null, - null, - null, - "context1", - VulnerabilityProbability.HIGH, - HotspotStatus.FIXED, - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + const string primaryMessage = "PrimaryMessage1"; + var issue1 = CreateRaisedHotspotDto( + id: IssueWithFlowsAndQuickFixesUseCase.Issue1Id, + primaryMessage: primaryMessage, + textRange: null); var analysisIssues = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue1 }).ToList(); var issue = analysisIssues.SingleOrDefault() as AnalysisIssue; issue.Should().NotBeNull(); issue.PrimaryLocation.TextRange.Should().BeNull(); + issue.PrimaryLocation.Message.Should().Be(primaryMessage); } [TestMethod] public void GetAnalysisIssues_IssueWithQuickFixSplitIntoTwoFileEdits_ReturnsIssueWithSingleQuickFix() { - var issue = new RaisedIssueDto(Guid.NewGuid(), - "serverKey", - "ruleKey", - "PrimaryMessage", - DateTimeOffset.Now, - true, - false, - new TextRangeDto(1, 2, 3, 4), - [], + var issue = CreateRaisedIssueDto( + quickFixes: [ new QuickFixDto([ new FileEditDto(new FileUri("C:\\IssueFile.cs"), [new TextEditDto(new TextRangeDto(5, 6, 7, 8), "new text")]), new FileEditDto(new FileUri("C:\\IssueFile.cs"), [new TextEditDto(new TextRangeDto(9, 10, 11, 12), "another text")]), new FileEditDto(new FileUri("C:\\AnotherFile.cs"), [new TextEditDto(new TextRangeDto(20, 10, 21, 12), "skip this fix")]) - ], "QuickFix")], - "context", - new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + ], "QuickFix") + ]); var analysisIssues = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue }).ToList(); @@ -385,6 +286,22 @@ public void GetAnalysisIssues_IssueWithQuickFixSplitIntoTwoFileEdits_ReturnsIssu analysisIssues[0].Fixes[0].Should().BeAssignableTo().Which.Edits.Should().HaveCount(2); } + [TestMethod] + public void GetAnalysisIssues_IssueWithRoslynQuickFix_ReturnsIssueWithRoslynQuickFix() + { + var expectedId = Guid.NewGuid(); + var roslynQuickFix = new RoslynQuickFix(expectedId); + var serializedFix = roslynQuickFix.GetStorageValue(); + + var issue = CreateRaisedIssueDto(quickFixes: [new QuickFixDto([], serializedFix)]); + + var analysisIssues = testSubject.GetAnalysisIssues(new FileUri("C:\\IssueFile.cs"), new List { issue }).ToList(); + + analysisIssues.Should().NotBeNull(); + analysisIssues.Should().ContainSingle(); + analysisIssues[0].Fixes.Should().BeEquivalentTo(roslynQuickFix); + } + [TestMethod] public void GetAnalysisIssues_TwoIssuesAndOneIsInvalidIssueDto_ReturnsOneIssueAndLogsTheInvalidOne() { @@ -473,23 +390,9 @@ public void GetAnalysisIssues_Hotspot_AnalysisHotspotIssueHasHotspotStatus(Hotsp { var analysisIssues = testSubject.GetAnalysisIssues(fileUri, new List { - new(Guid.Empty, - default, - default, - default, - default, - default, - default, - new TextRangeDto(1, - 2, - 3, - 4), - [], - default, - default, - null, - hotspotStatus, - new StandardModeDetails(default, default)) + CreateRaisedHotspotDto( + vulnerabilityProbability: null, + status: hotspotStatus) }); var hotspotIssue = analysisIssues.SingleOrDefault() as AnalysisHotspotIssue; @@ -497,6 +400,68 @@ public void GetAnalysisIssues_Hotspot_AnalysisHotspotIssueHasHotspotStatus(Hotsp hotspotIssue.HotspotStatus.Should().Be(hotspotStatus.ToHotspotStatus()); } + + private static RaisedIssueDto CreateRaisedIssueDto( + Guid? id = null, + string serverKey = null, + string ruleKey = "rule:key", + string primaryMessage = "Primary message", + DateTimeOffset? introductionDate = null, + bool isOnNewCode = true, + bool resolved = false, + TextRangeDto textRange = null, + List flows = null, + List quickFixes = null, + string ruleDescriptionContextKey = null, + Either severityMode = null) + { + return new RaisedIssueDto( + id ?? Guid.NewGuid(), + serverKey, + ruleKey, + primaryMessage, + introductionDate ?? DateTimeOffset.Now, + isOnNewCode, + resolved, + textRange, + flows ?? [], + quickFixes ?? [], + ruleDescriptionContextKey, + severityMode ?? new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + } + + + private static RaisedHotspotDto CreateRaisedHotspotDto( + Guid? id = null, + string serverKey = null, + string ruleKey = "rule:key", + string primaryMessage = "Primary message", + DateTimeOffset? introductionDate = null, + bool isOnNewCode = true, + bool resolved = false, + TextRangeDto textRange = null, + List flows = null, + List quickFixes = null, + string ruleDescriptionContextKey = null, + VulnerabilityProbability? vulnerabilityProbability = VulnerabilityProbability.HIGH, + HotspotStatus status = HotspotStatus.TO_REVIEW, + Either severityMode = null) => + new( + id ?? Guid.NewGuid(), + serverKey, + ruleKey, + primaryMessage, + introductionDate ?? DateTimeOffset.Now, + isOnNewCode, + resolved, + textRange, + flows ?? [], + quickFixes ?? [], + ruleDescriptionContextKey, + vulnerabilityProbability, + status, + severityMode ?? new StandardModeDetails(IssueSeverity.MAJOR, RuleType.CODE_SMELL)); + private static class UnflattenedFlowsUseCase { internal static FileUri FileUri => new("C:\\IssueFile.cs"); diff --git a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs index 40eec6d75e..90484a350f 100644 --- a/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs +++ b/src/SLCore/Listener/Analysis/RaiseFindingToAnalysisIssueConverter.cs @@ -134,6 +134,11 @@ private static IAnalysisIssueFlow GetAnalysisIssueFlow(IEnumerable e.target == fileURi); if (fileEdits.Count == 0) { From a3238c67304f008b56ffa621c1dc06bb2c6244e6 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:16:26 +0200 Subject: [PATCH 27/38] SLVS-2554 Update SQVS Roslyn Plugin to 1.0.0.36 master build (#6405) [SLVS-2554](https://sonarsource.atlassian.net/browse/SLVS-2554) Part of SLVS-2337 [SLVS-2554]: https://sonarsource.atlassian.net/browse/SLVS-2554?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- src/EmbeddedSonarAnalyzer.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index e914faf7cd..e06f5ff06b 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0.29 + 1.0.0.36 10.34.0.83431 1.0.0 From f2badd7efc3ba77dd80fd42223ac4f34176c2e71 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:03:16 +0200 Subject: [PATCH 28/38] SLVS-2555 Fix QG (#6406) [SLVS-2555](https://sonarsource.atlassian.net/browse/SLVS-2555) Part of SLVS-2337 [SLVS-2555]: https://sonarsource.atlassian.net/browse/SLVS-2555?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- src/Core.UnitTests/LanguageTests.cs | 1 - .../Http/AnalysisRequestHandlerTest.cs | 8 +++---- .../Http/HttpRequestHandlerTest.cs | 4 ++-- .../Http/AnalysisRequestHandler.cs | 8 +++---- .../Http/HttpRequestHandler.cs | 6 ++--- .../Http/RoslynAnalysisHttpServer.cs | 16 +++++++------- .../Http/SecureStringExtensions.cs | 22 ++++++++++++++++++- 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/Core.UnitTests/LanguageTests.cs b/src/Core.UnitTests/LanguageTests.cs index cd30b5f6de..049e1c6e30 100644 --- a/src/Core.UnitTests/LanguageTests.cs +++ b/src/Core.UnitTests/LanguageTests.cs @@ -32,7 +32,6 @@ public void Language_Ctor_ArgChecks() // Arrange var key = "k"; var name = "MyName"; - var fileSuffix = "suffix"; var serverLanguageKey = "serverLanguageKey"; // Act + Assert diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs index bda3ffcf85..b9e0f6be38 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs @@ -237,7 +237,7 @@ public async Task ParseAnalysisRequestBody_DeserializationFails_ReturnsNull() request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBody(context); + var result = await testSubject.ParseAnalysisRequestBodyAsync(context); result.Should().BeNull(); } @@ -249,7 +249,7 @@ public async Task ParseAnalysisRequestBody_FileNamesMissing_ReturnsNull() request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBody(context); + var result = await testSubject.ParseAnalysisRequestBodyAsync(context); result.Should().BeNull(); } @@ -261,7 +261,7 @@ public async Task ParseAnalysisRequestBody_FileNamesEmpty_ReturnsNull() request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBody(context); + var result = await testSubject.ParseAnalysisRequestBodyAsync(context); result.Should().BeNull(); } @@ -274,7 +274,7 @@ public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBody(context); + var result = await testSubject.ParseAnalysisRequestBodyAsync(context); result.Should().NotBeNull(); result!.FileNames.Should().HaveCount(1); diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs index 427846cd54..2bbbeecaca 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs @@ -65,7 +65,7 @@ public async Task SendResponse_WritesCorrectlySerializedDiagnostics() response.OutputStream.Returns(new MemoryStream()); var expectedString = "{\"Diagnostics\":[{\"Id\":\"id1\"}]}"; - await testSubject.SendResponse(context, expectedString); + await testSubject.SendResponseAsync(context, expectedString); response.Received().ContentLength64 = Encoding.UTF8.GetBytes(expectedString).Length; response.Received().StatusCode = (int)HttpStatusCode.OK; @@ -77,7 +77,7 @@ public async Task SendResponse_ClosesOutputStream() var outputStream = new MemoryStream(); response.OutputStream.Returns(outputStream); - await testSubject.SendResponse(context, "{\"Diagnostics\":[}"); + await testSubject.SendResponseAsync(context, "{\"Diagnostics\":[}"); outputStream.CanRead.Should().BeFalse(); } diff --git a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs index 88076a4b1d..4dc3c3f281 100644 --- a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs +++ b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs @@ -32,7 +32,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; public interface IAnalysisRequestHandler { - Task ParseAnalysisRequestBody(IHttpListenerContext context); + Task ParseAnalysisRequestBodyAsync(IHttpListenerContext context); string ParseAnalysisRequestResponse(List diagnostics); @@ -69,9 +69,9 @@ public HttpStatusCode ValidateRequest(IHttpListenerContext context) return HttpStatusCode.OK; } - public async Task ParseAnalysisRequestBody(IHttpListenerContext context) + public async Task ParseAnalysisRequestBodyAsync(IHttpListenerContext context) { - var body = await ReadBody(context); + var body = await ReadBodyAsync(context); var requestDto = GetAnalysisRequestFromBody(body); if (requestDto != null && requestDto.FileNames.Count != 0) { @@ -88,7 +88,7 @@ public string ParseAnalysisRequestResponse(List diagnostics) return responseString; } - private static async Task ReadBody(IHttpListenerContext context) + private static async Task ReadBodyAsync(IHttpListenerContext context) { using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding); return await reader.ReadToEndAsync(); diff --git a/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs b/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs index 5e7231f846..81603b82f1 100644 --- a/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs +++ b/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs @@ -27,7 +27,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; public interface IHttpRequestHandler { - Task SendResponse(IHttpListenerContext context, string responseString); + Task SendResponseAsync(IHttpListenerContext context, string responseString); void CloseRequest(IHttpListenerContext context, HttpStatusCode statusCode); } @@ -43,9 +43,9 @@ public void CloseRequest(IHttpListenerContext context, HttpStatusCode statusCode context.Response.Close(); } - public async Task SendResponse(IHttpListenerContext context, string responseString) => await WriteResponse(responseString, context, HttpStatusCode.OK); + public async Task SendResponseAsync(IHttpListenerContext context, string responseString) => await WriteResponseAsync(responseString, context, HttpStatusCode.OK); - private static async Task WriteResponse(string responseString, IHttpListenerContext context, HttpStatusCode statusCode) + private static async Task WriteResponseAsync(string responseString, IHttpListenerContext context, HttpStatusCode statusCode) { var buffer = Encoding.UTF8.GetBytes(responseString); context.Response.ContentLength64 = buffer.Length; diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs index d38331ff96..339959c1d6 100644 --- a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -90,7 +90,7 @@ private async Task StartListenAsync(int attempt, int port) { httpListener!.Start(); logger.LogVerbose(Resources.HttpServerStarted); - await WaitForRequests(httpListener, cancellationTokenSource.Token); + await WaitForRequestsAsync(httpListener, cancellationTokenSource.Token); } catch (HttpListenerException ex) { @@ -98,7 +98,7 @@ private async Task StartListenAsync(int attempt, int port) } } - private async Task WaitForRequests(HttpListener listener, CancellationToken cancellationToken) + private async Task WaitForRequestsAsync(HttpListener listener, CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { @@ -112,7 +112,7 @@ private async Task WaitForRequests(HttpListener listener, CancellationToken canc break; } context = new HttpListenerContextAdapter(await getRequestTask); - _ = HandleRequestWithTimeout(context, cancellationToken); + _ = HandleRequestWithTimeoutAsync(context, cancellationToken); } catch (Exception ex) { @@ -125,13 +125,13 @@ private async Task WaitForRequests(HttpListener listener, CancellationToken canc } } - private async Task HandleRequestWithTimeout(IHttpListenerContext context, CancellationToken serverCancellationToken) + private async Task HandleRequestWithTimeoutAsync(IHttpListenerContext context, CancellationToken serverCancellationToken) { using var requestCancellationToken = new CancellationTokenSource(settings.RequestMillisecondsTimeout); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(serverCancellationToken, requestCancellationToken.Token); try { - await Task.Run(() => HandleRequest(context, linkedCts.Token), linkedCts.Token); + await Task.Run(() => HandleRequestAsync(context, linkedCts.Token), linkedCts.Token); } catch (OperationCanceledException) { @@ -145,14 +145,14 @@ private async Task HandleRequestWithTimeout(IHttpListenerContext context, Cancel } } - private async Task HandleRequest(IHttpListenerContext context, CancellationToken cancellationToken) + private async Task HandleRequestAsync(IHttpListenerContext context, CancellationToken cancellationToken) { if (analysisRequestHandler.ValidateRequest(context) is var validationStatusCode && validationStatusCode != HttpStatusCode.OK) { httpRequestHandler.CloseRequest(context, validationStatusCode); return; } - if (await analysisRequestHandler.ParseAnalysisRequestBody(context) is not { } analysisRequest) + if (await analysisRequestHandler.ParseAnalysisRequestBodyAsync(context) is not { } analysisRequest) { httpRequestHandler.CloseRequest(context, HttpStatusCode.BadRequest); return; @@ -160,6 +160,6 @@ private async Task HandleRequest(IHttpListenerContext context, CancellationToken var issues = await roslynAnalysisService.AnalyzeAsync(analysisRequest, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - await httpRequestHandler.SendResponse(context, analysisRequestHandler.ParseAnalysisRequestResponse(issues.ToList())); + await httpRequestHandler.SendResponseAsync(context, analysisRequestHandler.ParseAnalysisRequestResponse(issues.ToList())); } } diff --git a/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs b/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs index 572afac3c4..2d2dfa6281 100644 --- a/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs +++ b/src/RoslynAnalyzerServer/Http/SecureStringExtensions.cs @@ -1,4 +1,24 @@ -using System.Runtime.InteropServices; +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Runtime.InteropServices; using System.Security; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; From 493f2c023645b10854d9ff7171e8cbc4ef3bcc02 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:56:14 +0200 Subject: [PATCH 29/38] SLVS-2474 Add resources for log messages & contexts used by the engine classes (#6411) --- .../RoslynProjectCompilationProviderTests.cs | 4 + ...lynSolutionAnalysisCommandProviderTests.cs | 6 +- .../SequentialRoslynAnalysisEngineTests.cs | 4 + .../Analysis/RoslynFileSemanticAnalysis.cs | 2 +- .../Analysis/RoslynFileSyntaxAnalysis.cs | 2 +- .../RoslynProjectCompilationProvider.cs | 2 +- .../RoslynSolutionAnalysisCommandProvider.cs | 6 +- .../SequentialRoslynAnalysisEngine.cs | 4 +- .../ApplyChangesOperation.Roslyn.cs | 4 +- .../Resources.Designer.cs | 81 +++++++++++++++++++ src/RoslynAnalyzerServer/Resources.resx | 27 +++++++ 11 files changed, 131 insertions(+), 11 deletions(-) diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs index 45ba3d60cc..acc9b2c5d0 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynProjectCompilationProviderTests.cs @@ -81,6 +81,10 @@ public void MefCtor_CheckIsExported() => [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] + public void Ctor_SetsLogContext() => + logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerExceptionLogContext); + [TestMethod] public async Task GetProjectCompilationAsync_ConfiguresCompilationWithCorrectOptions() { diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs index 204f4a4639..08a537f98b 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/RoslynSolutionAnalysisCommandProviderTests.cs @@ -49,7 +49,7 @@ public class RoslynSolutionAnalysisCommandProviderTests public void TestInitialize() { workspaceWrapper = Substitute.For(); - logger = new TestLogger(); + logger = Substitute.ForPartsOf(); solutionWrapper = Substitute.For(); workspaceWrapper.GetCurrentSolution().Returns(solutionWrapper); testSubject = new RoslynSolutionAnalysisCommandProvider(workspaceWrapper, logger); @@ -65,6 +65,10 @@ public void MefCtor_CheckIsExported() => public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] + public void Ctor_SetsLogContext() => + logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); + [TestMethod] public void GetAnalysisCommandsForCurrentSolution_NoProjects_ReturnsEmptyList() { diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs index 426bd08399..b67129575d 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs @@ -70,6 +70,10 @@ public void MefCtor_CheckIsExported() => [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + [TestMethod] + public void Ctor_SetsLogContext() => + logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisEngineLogContext); + [TestMethod] public async Task AnalyzeAsync_EmptyAnalysisCommands_ReturnsEmptyCollection() { diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs b/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs index b6e3a46d25..d8666597af 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynFileSemanticAnalysis.cs @@ -34,7 +34,7 @@ public async Task> ExecuteAsync(IRoslynCompilationWit var semanticModel = compilation.GetSemanticModel(AnalysisFilePath); if (semanticModel == null) { - logger.LogVerbose("No semantic model found for {0}", AnalysisFilePath); + logger.LogVerbose(Resources.AnalysisCommand_NoSemanticModel, AnalysisFilePath); return ImmutableArray.Empty; } diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs b/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs index a40b20cbda..032febf914 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynFileSyntaxAnalysis.cs @@ -34,7 +34,7 @@ public async Task> ExecuteAsync(IRoslynCompilationWit var syntaxTree = compilation.GetSyntaxTree(AnalysisFilePath); if (syntaxTree == null) { - logger.LogVerbose("No syntax tree found for {0}", AnalysisFilePath); + logger.LogVerbose(Resources.AnalysisCommand_NoSyntaxTree, AnalysisFilePath); return ImmutableArray.Empty; } diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs index 7e31a4e9d8..29c8db91cb 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynProjectCompilationProvider.cs @@ -33,7 +33,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; [method: ImportingConstructor] internal class RoslynProjectCompilationProvider(ILogger logger) : IRoslynProjectCompilationProvider { - private readonly ILogger analyzerExceptionLogger = logger.ForContext("Roslyn Analysis", "Analyzer Exception"); + private readonly ILogger analyzerExceptionLogger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisAnalyzerExceptionLogContext); public async Task GetProjectCompilationAsync( IRoslynProjectWrapper project, diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs b/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs index f66cff85bf..b3eef5b195 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynSolutionAnalysisCommandProvider.cs @@ -31,7 +31,7 @@ internal class RoslynSolutionAnalysisCommandProvider( IRoslynWorkspaceWrapper roslynWorkspaceWrapper, ILogger logger) : IRoslynSolutionAnalysisCommandProvider { - private readonly ILogger logger = logger.ForContext("Roslyn Analysis", "Configuration"); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisConfigurationLogContext); public List GetAnalysisCommandsForCurrentSolution(string[] filePaths) { @@ -43,7 +43,7 @@ public List GetAnalysisCommandsForCurrentSolution( { if (!project.SupportsCompilation) { - logger.LogVerbose("Project {0} does not support compilation", project.Name); + logger.LogVerbose(Resources.AnalysisCommandProvider_NoCompilationForProject, project.Name); continue; } @@ -57,7 +57,7 @@ public List GetAnalysisCommandsForCurrentSolution( if (!result.Any()) { - logger.WriteLine("No projects to analyze"); + logger.WriteLine(Resources.AnalysisCommandProvider_NoProjects); } return result; diff --git a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs index 7c9217f445..ea62f867bc 100644 --- a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs @@ -33,7 +33,7 @@ internal class SequentialRoslynAnalysisEngine( IRoslynQuickFixFactory quickFixFactory, ILogger logger) : IRoslynAnalysisEngine { - private readonly ILogger logger = logger.ForContext("Roslyn Analysis", "Engine"); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynAnalysisLogContext, Resources.RoslynAnalysisEngineLogContext); public async Task> AnalyzeAsync( List projectsAnalysis, @@ -62,7 +62,7 @@ public async Task> AnalyzeAsync( // todo SLVS-2468 improve issue merging if (!uniqueDiagnostics.Add(roslynIssue)) { - logger.LogVerbose("Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}", roslynIssue.RuleId, Path.GetFileName(roslynIssue.PrimaryLocation.FilePath), roslynIssue.PrimaryLocation.TextRange.StartLine); + logger.LogVerbose(Resources.AnalysisEngine_DuplicateDiagnostic, roslynIssue.RuleId, Path.GetFileName(roslynIssue.PrimaryLocation.FilePath), roslynIssue.PrimaryLocation.TextRange.StartLine); } } } diff --git a/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs index 8934eab1d5..45ddc08a90 100644 --- a/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs +++ b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs @@ -43,7 +43,7 @@ internal static async Task ApplyOrMergeChangesAsync( if (SolutionChangedCritically(solutionChanges)) { - logger.LogVerbose("Solution projects have changed, update no longer valid"); + logger.LogVerbose(Resources.ApplyChangesOperation_SolutionChanged); return false; // todo https://sonarsource.atlassian.net/browse/SLVS-2513 this will lead to invalid quickfixes if project configuration changes. // do we need to reanalyze open files on major workspace changes? can modified analyzer references be ignored? @@ -58,7 +58,7 @@ internal static async Task ApplyOrMergeChangesAsync( // We only support text changes. If we see any other changes to this project, bail out immediately. if (ProjectChangedCritically(changedProject)) { - logger.LogVerbose("Project {0} has changed, update no longer valid", changedProject.NewProject.Name); + logger.LogVerbose(Resources.ApplyChangesOperation_ProjectChanged, changedProject.NewProject.Name); return false; } diff --git a/src/RoslynAnalyzerServer/Resources.Designer.cs b/src/RoslynAnalyzerServer/Resources.Designer.cs index b5bd971783..384c2bb96f 100644 --- a/src/RoslynAnalyzerServer/Resources.Designer.cs +++ b/src/RoslynAnalyzerServer/Resources.Designer.cs @@ -59,6 +59,69 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to No semantic model found for {0}. + /// + internal static string AnalysisCommand_NoSemanticModel { + get { + return ResourceManager.GetString("AnalysisCommand_NoSemanticModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "No syntax tree found for {0}". + /// + internal static string AnalysisCommand_NoSyntaxTree { + get { + return ResourceManager.GetString("AnalysisCommand_NoSyntaxTree", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project {0} does not support compilation. + /// + internal static string AnalysisCommandProvider_NoCompilationForProject { + get { + return ResourceManager.GetString("AnalysisCommandProvider_NoCompilationForProject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No projects to analyze. + /// + internal static string AnalysisCommandProvider_NoProjects { + get { + return ResourceManager.GetString("AnalysisCommandProvider_NoProjects", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}". + /// + internal static string AnalysisEngine_DuplicateDiagnostic { + get { + return ResourceManager.GetString("AnalysisEngine_DuplicateDiagnostic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project {0} has changed, update no longer valid. + /// + internal static string ApplyChangesOperation_ProjectChanged { + get { + return ResourceManager.GetString("ApplyChangesOperation_ProjectChanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Solution projects have changed, update no longer valid. + /// + internal static string ApplyChangesOperation_SolutionChanged { + get { + return ResourceManager.GetString("ApplyChangesOperation_SolutionChanged", resourceCulture); + } + } + /// /// Looks up a localized string similar to Content length exceeded. Received {0} bytes, max allowed: {1} bytes.. /// @@ -185,6 +248,15 @@ internal static string RoslynAnalysisAnalyzerClassLoaderFailedToLoad { } } + /// + /// Looks up a localized string similar to Analyzer Exception. + /// + internal static string RoslynAnalysisAnalyzerExceptionLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisAnalyzerExceptionLogContext", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to load analyzer {0}: {1}. /// @@ -230,6 +302,15 @@ internal static string RoslynAnalysisConfigurationNoAnalyzers { } } + /// + /// Looks up a localized string similar to Engine. + /// + internal static string RoslynAnalysisEngineLogContext { + get { + return ResourceManager.GetString("RoslynAnalysisEngineLogContext", resourceCulture); + } + } + /// /// Looks up a localized string similar to Analysis. /// diff --git a/src/RoslynAnalyzerServer/Resources.resx b/src/RoslynAnalyzerServer/Resources.resx index 218cd6936e..7e23d8add9 100644 --- a/src/RoslynAnalyzerServer/Resources.resx +++ b/src/RoslynAnalyzerServer/Resources.resx @@ -153,6 +153,12 @@ Staring http server for roslyn analysis... + + Solution projects have changed, update no longer valid + + + Project {0} has changed, update no longer valid + Location {0} @@ -165,6 +171,12 @@ Configuration + + Engine + + + Analyzer Exception + No analyzers loaded for language {0} @@ -183,4 +195,19 @@ QuickFix + + No projects to analyze + + + Project {0} does not support compilation + + + No semantic model found for {0} + + + "No syntax tree found for {0}" + + + "Duplicate diagnostic discarded ID: {0}, File: {1}, Line: {2}" + \ No newline at end of file From d3af700ee28667ce68ecc1ddd24c7e9faa39d71d Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:02:35 +0200 Subject: [PATCH 30/38] SLVS-2550 Improve debounce action internals to not rely on TaskCanceledException (#6404) --- .../ResettableOneShotTimerTests.cs | 53 ++++++++++ .../SonarLintTagger/TaggerProviderTests.cs | 2 +- .../TaskExecutorWithDebounceFactoryTest.cs | 10 +- .../TaskExecutorWithDebounceTest.cs | 98 ++++++++++++------- .../TextBufferIssueTrackerTests.cs | 4 +- .../SonarLintTagger/ResettableOneShotTimer.cs | 48 +++++++++ .../TaskExecutorWithDebounce.cs | 72 ++++++++------ .../SonarLintTagger/TextBufferIssueTracker.cs | 6 +- 8 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs create mode 100644 src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs new file mode 100644 index 0000000000..a168ff228f --- /dev/null +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/ResettableOneShotTimerTests.cs @@ -0,0 +1,53 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; + +[TestClass] +public class ResettableOneShotTimerTests +{ + [TestMethod] + public async Task ResettableOneShotTimer_SmokeTest() + { + var timerTimeSpan = TimeSpan.FromMilliseconds(100); + var testSubject = new ResettableOneShotTimer(timerTimeSpan); + var eventHandler = Substitute.For(); + testSubject.Elapsed += eventHandler; + + testSubject.Reset(); + await Task.Delay(2 * (int)timerTimeSpan.TotalMilliseconds); + + eventHandler.ReceivedWithAnyArgs(1).Invoke(default, default); + } + + [TestMethod] + public void ResettableOneShotTimer_Dispose_DisposesTimer() + { + var timerTimeSpan = TimeSpan.FromMilliseconds(50); + var testSubject = new ResettableOneShotTimer(timerTimeSpan); + + testSubject.Dispose(); + + var act = () => testSubject.Reset(); + act.Should().Throw(); + } +} diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs index fd20a77f96..5264f5dae6 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs @@ -152,7 +152,7 @@ public void CreateTagger_should_create_tracker_when_tagger_is_created() tagger.Should().NotBeNull(); VerifyCreateIssueConsumerWasCalled(doc); - taskExecutorWithDebounceFactory.Received(1).Create(debounceMilliseconds: TimeSpan.FromMilliseconds(500)); + taskExecutorWithDebounceFactory.Received(1).Create(debounceTimeSpan: TimeSpan.FromMilliseconds(500)); } [TestMethod] diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs index 40aac54c3e..da94c5fd1b 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceFactoryTest.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Synchronization; using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; @@ -27,19 +28,14 @@ namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; public class TaskExecutorWithDebounceFactoryTest { private TaskExecutorWithDebounceFactory testSubject; - private IAsyncLockFactory asyncLockFactory; [TestInitialize] - public void TestInitialize() - { - asyncLockFactory = Substitute.For(); - testSubject = new TaskExecutorWithDebounceFactory(asyncLockFactory); - } + public void TestInitialize() => testSubject = new TaskExecutorWithDebounceFactory(Substitute.For()); [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport()); [TestMethod] public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs index 2cc88aa22f..7bd93b53e0 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaskExecutorWithDebounceTest.cs @@ -18,9 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.Integration.TestInfrastructure; using SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; @@ -28,62 +25,89 @@ namespace SonarLint.VisualStudio.Integration.UnitTests.SonarLintTagger; [TestClass] public class TaskExecutorWithDebounceTest { - private readonly TimeSpan debounceTimeInMs = TimeSpan.FromMilliseconds(100); - private IAsyncLock asyncLock; - private IAsyncLockFactory asyncLockFactory; private TaskExecutorWithDebounce testSubject; + private NoOpThreadHandler threadHandling; + private IResettableOneShotTimer timer; [TestInitialize] public void TestInitialize() { - asyncLockFactory = Substitute.For(); - asyncLock = Substitute.For(); - asyncLockFactory.Create().Returns(asyncLock); - testSubject = new TaskExecutorWithDebounce(asyncLockFactory, debounceTimeInMs); + threadHandling = Substitute.ForPartsOf(); + timer = Substitute.For(); + testSubject = new TaskExecutorWithDebounce(timer, threadHandling); } [TestMethod] - public async Task DebounceAsync_ExecutesTaskWithDebounce() + public void Debounce_TimerNotRaised_DoesNotExecuteAction() { - var currentState = new TestData { Value = 1 }; - var tcs = new TaskCompletionSource(); - var stopwatch = Stopwatch.StartNew(); + var action = Substitute.For(); - testSubject.DebounceAsync(() => + testSubject.Debounce(action); + + action.DidNotReceive().Invoke(); + timer.Received().Reset(); + } + + [TestMethod] + public void Debounce_NoActionSet_DoesNotThrow() + { + var act = () => timer.Elapsed += Raise.Event(); + + act.Should().NotThrow(); + } + + [TestMethod] + public void Debounce_ExecutesTaskWithDebounce() + { + var action = Substitute.For(); + testSubject.Debounce(action); + + timer.Elapsed += Raise.Event(); + + Received.InOrder(() => { - UpdateState(currentState, 2, tcs); - stopwatch.Stop(); - }).Forget(); - await tcs.Task; - - asyncLock.Received(1).AcquireAsync().IgnoreAwaitForAssert(); - currentState.Value.Should().Be(2); - stopwatch.ElapsedMilliseconds.Should().BeGreaterOrEqualTo(debounceTimeInMs.Milliseconds); + timer.Reset(); + threadHandling.RunOnBackgroundThread(Arg.Any>>()); + action.Invoke(); + }); } [TestMethod] - public async Task DebounceAsync_MultipleTimes_UpdatesWithLatestState() + public void Debounce_MultipleTimes_UpdatesWithLatestState() { - var currentState = new TestData { Value = 1 }; - var tcs = new TaskCompletionSource(); + var action1 = Substitute.For(); + var action2 = Substitute.For(); + var action3 = Substitute.For(); + testSubject.Debounce(action1); + testSubject.Debounce(action2); + testSubject.Debounce(action3); - testSubject.DebounceAsync(() => UpdateState(currentState, 2)).Forget(); - testSubject.DebounceAsync(() => UpdateState(currentState, 3)).Forget(); - testSubject.DebounceAsync(() => UpdateState(currentState, 4, tcs)).Forget(); - await tcs.Task; + timer.Elapsed += Raise.Event(); - asyncLock.Received(3).AcquireAsync().IgnoreAwaitForAssert(); - currentState.Value.Should().Be(4); + action1.DidNotReceive().Invoke(); + action2.DidNotReceive().Invoke(); + action3.Received().Invoke(); } - private static void UpdateState(TestData date, int newValue, TaskCompletionSource taskCompletionSource = null) + [TestMethod] + public void Debounce_MultipleTriggers_ActionOnlyExecutedOnce() { - date.Value = newValue; - taskCompletionSource?.SetResult(1); + var action = Substitute.For(); + testSubject.Debounce(action); + + timer.Elapsed += Raise.Event(); + timer.Elapsed += Raise.Event(); + timer.Elapsed += Raise.Event(); + + action.Received(1).Invoke(); } - private record TestData + [TestMethod] + public void Dispose_DisposesTimer() { - public int Value { get; set; } + testSubject.Dispose(); + + timer.Received(1).Dispose(); + timer.Received(1).Elapsed -= Arg.Any(); } } diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs index 09fe5b0d38..f7365644d0 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs @@ -157,6 +157,8 @@ public void Dispose_CleansUpEventsAndRegistrations() mockedJavascriptDocumentFooJs.Received(1).FileActionOccurred -= Arg.Any>(); ((ITextBuffer2)mockDocumentTextBuffer).Received(1).ChangedOnBackground -= Arg.Any>(); + + taskExecutorWithDebounce.Received(1).Dispose(); } [TestMethod] @@ -490,7 +492,7 @@ private EventHandler SubscribeToDocumentSaved() } private void MockTaskExecutorWithDebounce() => - taskExecutorWithDebounce.When(x => x.DebounceAsync(Arg.Any())).Do(callInfo => + taskExecutorWithDebounce.When(x => x.Debounce(Arg.Any())).Do(callInfo => { var action = callInfo.Arg(); action(); diff --git a/src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs b/src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs new file mode 100644 index 0000000000..1062337dac --- /dev/null +++ b/src/Integration.Vsix/SonarLintTagger/ResettableOneShotTimer.cs @@ -0,0 +1,48 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; + +internal interface IResettableOneShotTimer : IDisposable +{ + void Reset(); + + event EventHandler Elapsed; +} + +internal sealed class ResettableOneShotTimer : IResettableOneShotTimer +{ + private readonly Timer timer; + private readonly long timerDurationMs; + + public ResettableOneShotTimer(TimeSpan timerTimeSpan) + { + timerDurationMs = (long)timerTimeSpan.TotalMilliseconds; + timer = new Timer(TimerAction, null, Timeout.Infinite, Timeout.Infinite); + } + + private void TimerAction(object state) => Elapsed?.Invoke(this, EventArgs.Empty); + + public void Reset() => timer.Change(timerDurationMs, Timeout.Infinite); + + public event EventHandler Elapsed; + + public void Dispose() => timer?.Dispose(); +} diff --git a/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs b/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs index 260a69c005..40c51eef4b 100644 --- a/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs +++ b/src/Integration.Vsix/SonarLintTagger/TaskExecutorWithDebounce.cs @@ -20,61 +20,69 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Core; namespace SonarLint.VisualStudio.Integration.Vsix.SonarLintTagger; internal interface ITaskExecutorWithDebounceFactory { - ITaskExecutorWithDebounce Create(TimeSpan debounceMilliseconds); + ITaskExecutorWithDebounce Create(TimeSpan debounceTimeSpan); } -internal interface ITaskExecutorWithDebounce +internal interface ITaskExecutorWithDebounce : IDisposable { - Task DebounceAsync(Action task); + void Debounce(Action task); } [Export(typeof(ITaskExecutorWithDebounceFactory))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class TaskExecutorWithDebounceFactory(IAsyncLockFactory asyncLockFactory) : ITaskExecutorWithDebounceFactory +internal class TaskExecutorWithDebounceFactory(IThreadHandling threadHandling) : ITaskExecutorWithDebounceFactory { - public ITaskExecutorWithDebounce Create(TimeSpan debounceMilliseconds) => new TaskExecutorWithDebounce(asyncLockFactory, debounceMilliseconds); + public ITaskExecutorWithDebounce Create(TimeSpan debounceTimeSpan) => new TaskExecutorWithDebounce(new ResettableOneShotTimer(debounceTimeSpan), threadHandling); } -internal class TaskExecutorWithDebounce(IAsyncLockFactory asyncLockFactory, TimeSpan debounceMilliseconds) : ITaskExecutorWithDebounce +internal sealed class TaskExecutorWithDebounce : ITaskExecutorWithDebounce { - private sealed record Debounce(CancellationTokenSource CancellationTokenSource); - private Debounce latestDebounceState; - private readonly IAsyncLock asyncLock = asyncLockFactory.Create(); + private readonly IThreadHandling threadHandling; + private readonly object locker = new(); + private readonly IResettableOneShotTimer timer; + private Action latestDebounceState; - public async Task DebounceAsync(Action task) + internal TaskExecutorWithDebounce(IResettableOneShotTimer timerWrapper, IThreadHandling threadHandling) { - Debounce latestState; - using (await asyncLock.AcquireAsync()) + this.threadHandling = threadHandling; + timer = timerWrapper; + timer.Elapsed += DebounceAction; + } + + private void DebounceAction(object state, EventArgs eventArgs) + { + Action action; + lock (locker) { - latestDebounceState?.CancellationTokenSource.Cancel(); - latestDebounceState = new Debounce(new CancellationTokenSource()); - latestState = latestDebounceState; + action = latestDebounceState; + latestDebounceState = null; } - ExecuteAction(task, latestState); + if (action != null) + { + threadHandling.RunOnBackgroundThread(action).Forget(); + } } - private void ExecuteAction(Action task, Debounce latestState) => - Task.Run(async () => + public void Debounce(Action task) + { + lock (locker) { - try - { - await Task.Delay(debounceMilliseconds, latestState.CancellationTokenSource.Token); - if (!latestState.CancellationTokenSource.Token.IsCancellationRequested) - { - task(); - } - } - catch (TaskCanceledException) - { - // do nothing - } - }, latestState.CancellationTokenSource.Token).Forget(); + latestDebounceState = task; + timer.Reset(); + } + } + + public void Dispose() + { + timer.Elapsed -= DebounceAction; + timer.Dispose(); + } } diff --git a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs index 66ac66cfbe..696742758b 100644 --- a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs +++ b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs @@ -20,7 +20,6 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Integration.Vsix.Analysis; @@ -114,6 +113,7 @@ public void Dispose() textBuffer2.ChangedOnBackground -= TextBuffer_OnChangedOnBackground; } sonarErrorDataSource.RemoveFactory(Factory); + taskExecutorWithDebounce.Dispose(); Provider.OnDocumentClosed(this); } @@ -199,10 +199,10 @@ private static void ClearErrorList(string filePath, IIssueConsumer issueConsumer } private void TextBuffer_OnChangedOnBackground(object sender, TextContentChangedEventArgs e) => - taskExecutorWithDebounce.DebounceAsync(() => + taskExecutorWithDebounce.Debounce(() => { var textSnapshot = e.After; UpdateAnalysisState(textSnapshot); Provider.OnDocumentUpdated(document.FilePath, textSnapshot.GetText(), DetectedLanguages); - }).Forget(); + }); } From e6cd54eaa3f8c7e1c94622bc94554cfc96746012 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:52:56 +0200 Subject: [PATCH 31/38] SLVS-2571 Improve UX analysis experience while typing (#6427) [SLVS-2571](https://sonarsource.atlassian.net/browse/SLVS-2571) [SLVS-2571]: https://sonarsource.atlassian.net/browse/SLVS-2571?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../TextBufferIssueTrackerTests.cs | 15 ++------------- .../SonarLintTagger/TextBufferIssueTracker.cs | 7 ------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs index f7365644d0..60eae8e849 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs @@ -301,17 +301,6 @@ public void UpdateAnalysisState_NoProjectInformation_CreatesIssueConsumerCorrect VerifyCreateIssueConsumerWasCalled(textDocument, (default, Guid.Empty), consumer, new AnalysisSnapshot(textDocument.FilePath, textDocument.TextBuffer.CurrentSnapshot)); } - [TestMethod] - public void UpdateAnalysisState_ClearsErrorList() - { - var textDocument = mockedJavascriptDocumentFooJs; - - CreateTestSubject(textDocument).UpdateAnalysisState(); - - issueConsumer.Received().SetIssues(textDocument.FilePath, []); - issueConsumer.Received().SetHotspots(textDocument.FilePath, []); - } - [TestMethod] public void UpdateAnalysisState_NonCriticalException_IsSuppressed() { @@ -509,8 +498,8 @@ private void VerifyAnalysisStateUpdated( issueConsumerFactory.Received().Create(textDocument, newAnalysisSnapshot.FilePath, newAnalysisSnapshot.TextSnapshot, Arg.Any(), Arg.Any(), Arg.Any()); issueConsumerStorage.Received().Set(textDocument.FilePath, Arg.Any()); - issueConsumer.Received().SetIssues(textDocument.FilePath, []); - issueConsumer.Received().SetHotspots(textDocument.FilePath, []); + issueConsumer.DidNotReceiveWithAnyArgs().SetIssues(default, default); + issueConsumer.DidNotReceiveWithAnyArgs().SetHotspots(default, default); eventHandler.Received().Invoke(taggerProvider, Arg.Is(x => x.Document.FullPath == textDocument.FilePath && x.Document.DetectedLanguages == javascriptLanguage diff --git a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs index 696742758b..e7793b43a6 100644 --- a/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs +++ b/src/Integration.Vsix/SonarLintTagger/TextBufferIssueTracker.cs @@ -189,13 +189,6 @@ private void CreateIssueConsumer(AnalysisSnapshot analysisSnapshot) var (projectName, projectGuid) = vsProjectInfoProvider.GetDocumentProjectInfo(analysisSnapshot.FilePath); var issueConsumer = issueConsumerFactory.Create(document, analysisSnapshot.FilePath, analysisSnapshot.TextSnapshot, projectName, projectGuid, SnapToNewSnapshot); issueConsumerStorage.Set(analysisSnapshot.FilePath, issueConsumer); - ClearErrorList(analysisSnapshot.FilePath, issueConsumer); - } - - private static void ClearErrorList(string filePath, IIssueConsumer issueConsumer) - { - issueConsumer.SetIssues(filePath, []); - issueConsumer.SetHotspots(filePath, []); } private void TextBuffer_OnChangedOnBackground(object sender, TextContentChangedEventArgs e) => From a9f987ad094d93c86987da314780ee3069de2ade Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:34:19 +0200 Subject: [PATCH 32/38] SLVS-2579 Update SQVS Roslyn and SLCore versions to latest branch builds (#6433) [SLVS-2579](https://sonarsource.atlassian.net/browse/SLVS-2579) Part of SLVS-2336 [SLVS-2579]: https://sonarsource.atlassian.net/browse/SLVS-2579?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- src/EmbeddedSonarAnalyzer.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index e06f5ff06b..8d37f18672 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0.36 + 1.0.0.54 10.34.0.83431 1.0.0 From 39bf9d06fcd672d56b1d3ac37d4f17186cc377d5 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:13:05 +0200 Subject: [PATCH 33/38] SLVS-2512 Implement roslyn analysis cancellation (#6457) Part of --- src/EmbeddedSonarAnalyzer.props | 2 +- .../Http/Helper/HttpRequester.cs | 8 +- .../Http/Helper/HttpServerStarter.cs | 2 +- .../Http/RoslynAnalysisHttpServerTest.cs | 93 +++++++++--- .../Http/AnalysisRequestHandlerTest.cs | 117 ++++++++++---- .../Http/HttpRequestHandlerTest.cs | 29 ++++ .../Http/RoslynAnalysisHttpServerTest.cs | 2 +- .../RoslynAnalysisServiceTests.cs | 115 +++++++++++++- .../Http/AnalysisRequestHandler.cs | 143 +++++++++++------- .../Http/HttpRequestHandler.cs | 10 ++ .../Models/AnalysisCancellationRequest.cs | 29 ++++ .../Http/Models/AnalysisRequest.cs | 5 + .../Http/RoslynAnalysisHttpServer.cs | 54 +++++-- .../IRoslynAnalysisService.cs | 1 + .../RoslynAnalysisService.cs | 57 ++++++- 15 files changed, 527 insertions(+), 140 deletions(-) create mode 100644 src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 8d37f18672..1fd9ea8918 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0.54 + 1.0.0.81 10.34.0.83431 1.0.0 diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs index 1f87c6010a..d928fba3b2 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpRequester.cs @@ -8,7 +8,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; -internal record AnalysisRequestConfig(SecureString Token, string RequestUri, params string[] FileNames); +internal record AnalysisRequestConfig(SecureString Token, string RequestUri, T Request); internal sealed class HttpRequester : IDisposable { @@ -25,11 +25,9 @@ public HttpRequester(int requestTimeout = WaitForServerMsTimeout) public void Dispose() => httpClient.Dispose(); - internal async Task SendRequest(AnalysisRequestConfig analysisRequestConfig) + internal async Task SendRequest(AnalysisRequestConfig analysisRequestConfig) { - var fileNames = analysisRequestConfig.FileNames.Select(x => new FileUri(x)); - var analysisRequest = new AnalysisRequest { FileNames = [.. fileNames] }; - var body = JsonConvert.SerializeObject(analysisRequest); + var body = JsonConvert.SerializeObject(analysisRequestConfig.Request); return await SendRequest(analysisRequestConfig.Token.ToUnsecureString(), analysisRequestConfig.RequestUri, body); } diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs index b2345e84da..24f825ed13 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/Helper/HttpServerStarter.cs @@ -68,7 +68,7 @@ public void StartListeningOnBackgroundThread() private static ILogger CreateMockedLogger() { var logger = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); + logger.ForContext(Arg.Any()).Returns(logger); return logger; } diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs index c5ee57e74a..e3b71eaa68 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs @@ -23,9 +23,12 @@ using System.Security; using System.Text; using Newtonsoft.Json; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http.Helper; +using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.IntegrationTests.Http; @@ -69,7 +72,7 @@ public async Task StartListenAsync_StartsAfterDisposed_DoesNotStart() serverStarter.RoslynAnalysisHttpServer.Dispose(); await serverStarter.RoslynAnalysisHttpServer.StartListenAsync(); - await VerifyServerNotReachable(CreateClientRequestConfig(serverStarter)); // the timeout of the request should be reached + await VerifyServerNotReachable(CreateClientRequestConfig(serverStarter)); // the timeout of the request should be reached serverStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerStarted); serverStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerDisposed); } @@ -108,13 +111,40 @@ public async Task StartListenAsync_AnalysisRequestTakesLongerThanTimeout_ClosesR var millisecondTimeout = 5; using var serverStarter2 = new HttpServerStarter(useMockedServerSettings: true); MockServerSettings(serverStarter2.ServerSettings, requestTimeout: millisecondTimeout); - SimulateLongAnalysis(serverStarter2.MockedRoslynAnalysisService, millisecondTimeout * 2); + SimulateAnalysisRunsOutOfTime(serverStarter2.MockedRoslynAnalysisService); serverStarter2.StartListeningOnBackgroundThread(); var response = await HttpRequester.SendRequest(CreateClientRequestConfig(serverStarter2)); - serverStarter2.MockedLogger.Received(1).LogVerbose(Resources.HttpRequestTimedOut, Arg.Any()); response.StatusCode.Should().Be(HttpStatusCode.RequestTimeout); + serverStarter2.MockedLogger.Received(1).LogVerbose(Arg.Any(), Resources.HttpRequestTimedOut, Arg.Any()); + } + + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task Cancel_ReturnsExpectedResult(bool isCanceled) + { + var analysisId = Guid.NewGuid(); + ServerStarter.MockedRoslynAnalysisService.Cancel(Arg.Is(x => x.AnalysisId == analysisId)).Returns(isCanceled); + ServerStarter.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(CreateCancellationRequestConfig(ServerStarter, analysisId)); + + response.StatusCode.Should().Be(isCanceled ? HttpStatusCode.OK : HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task UnknownUrl_ReturnsBadRequest() + { + var unknownUrlRequest = new AnalysisRequestConfig(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token, + GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port, requestPath: "UNKNOWNURL"), + new { }); + ServerStarter.StartListeningOnBackgroundThread(); + + var response = await HttpRequester.SendRequest(unknownUrlRequest); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [TestMethod] @@ -179,7 +209,7 @@ public async Task StartListenAsync_InvalidRequestUri_ReturnsNotFound() var response = await HttpRequester.SendRequest(CreateClientRequestConfig(requestUri: invalidRequestUrl)); - response.StatusCode.Should().Be(HttpStatusCode.NotFound); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [TestMethod] @@ -203,7 +233,7 @@ public async Task StartListenAsync_AnalysisThrowsException_ReturnsInternalServer var response = await HttpRequester.SendRequest(CreateClientRequestConfig(serverStarter2)); response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - serverStarter2.MockedLogger.Received(1).LogVerbose(Resources.HttpRequestFailed, Arg.Is(x => x.Contains(exceptionMessage))); + serverStarter2.MockedLogger.Received(1).LogVerbose(Arg.Any(), Resources.HttpRequestFailed, Arg.Is(x => x.Contains(exceptionMessage))); } [TestMethod] @@ -214,7 +244,7 @@ public async Task Dispose_StopsServer() testServerStarter.RoslynAnalysisHttpServer.Dispose(); - await VerifyServerNotReachable(CreateClientRequestConfig(testServerStarter)); // the timeout of the request should be reached + await VerifyServerNotReachable(CreateClientRequestConfig(testServerStarter)); // the timeout of the request should be reached testServerStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerDisposed); } @@ -231,22 +261,37 @@ public void Dispose_CallsMultipleTimes_DisposesOnce() testServerStarter.MockedLogger.Received(1).LogVerbose(Resources.HttpServerDisposed); } - private static AnalysisRequestConfig CreateClientRequestConfig(HttpServerStarter httpServerStarter) => - CreateClientRequestConfig([CsharpFileName], GetRequestUrl(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port), - httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token); - - private static AnalysisRequestConfig CreateClientRequestConfig(SecureString? token = null, string? requestUri = null) => CreateClientRequestConfig([CsharpFileName], requestUri, token); - - private static AnalysisRequestConfig CreateClientRequestConfig(string[] fileNames, string? requestUri = null, SecureString? token = null) + private static AnalysisRequestConfig CreateClientRequestConfig(HttpServerStarter httpServerStarter, Guid? analysisId = null) => + CreateClientRequestConfig( + [CsharpFileName], + GetRequestUrl(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port), + httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token, + analysisId); + + private static AnalysisRequestConfig CreateCancellationRequestConfig(HttpServerStarter httpServerStarter, Guid analysisId) => + new(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token, + GetRequestUrl(httpServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port, requestPath: "cancel"), + new AnalysisCancellationRequest { AnalysisId = analysisId }); + + private static AnalysisRequestConfig CreateClientRequestConfig(SecureString? token = null, string? requestUri = null) => + CreateClientRequestConfig([CsharpFileName], requestUri, token); + + private static AnalysisRequestConfig CreateClientRequestConfig( + string[] fileNames, + string? requestUri = null, + SecureString? token = null, + Guid? analysisId = null) { token ??= ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token; requestUri ??= GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port); - return new AnalysisRequestConfig(token, requestUri, fileNames); + var fileUris = fileNames.Select(x => new FileUri(x)); + var analysisRequest = new AnalysisRequest { FileNames = [.. fileUris], ActiveRules = [new ActiveRuleDto("id", [])], AnalysisId = analysisId ?? Guid.NewGuid() }; + return new AnalysisRequestConfig(token, requestUri, analysisRequest); } private static string GetRequestUrl(int port, string requestPath = "analyze") => $"http://127.0.0.1:{port}/{requestPath}"; - private static async Task VerifyServerReachable(AnalysisRequestConfig requestConfig) + private static async Task VerifyServerReachable(AnalysisRequestConfig requestConfig) { var response = await HttpRequester.SendRequest(requestConfig); await VerifyRequestSucceeded(response); @@ -260,10 +305,10 @@ private static async Task VerifyRequestSucceeded(HttpResponseMessage response) analysisResponse!.RoslynIssues.Should().BeEmpty(); } - private static async Task VerifyServerNotReachable(AnalysisRequestConfig analysisRequestConfig) where T : Exception + private static async Task VerifyServerNotReachable(AnalysisRequestConfig analysisRequestConfig) where TException : Exception { var act = async () => await HttpRequester.SendRequest(analysisRequestConfig); - await act.Should().ThrowAsync(); + await act.Should().ThrowAsync(); } private static async Task GetAnalysisResponse(HttpResponseMessage response) @@ -276,7 +321,7 @@ private static async Task VerifyServerNotReachable(AnalysisRequestConfig anal private static void MockServerSettings( IHttpServerSettings serverConfiguration, int requestTimeout = 50, - int maxBodyLength = 100) + int maxBodyLength = 1024) { serverConfiguration.MaxStartAttempts.Returns(3); serverConfiguration.RequestMillisecondsTimeout.Returns(requestTimeout); @@ -285,8 +330,16 @@ private static void MockServerSettings( private static void MockServerConfiguration(IHttpServerConfigurationProvider serverConfigurationProvider, int port) => serverConfigurationProvider.CurrentConfiguration.Port.Returns(port); - private static void SimulateLongAnalysis(IRoslynAnalysisService roslynAnalysisService, int milliseconds) => + private static void SimulateAnalysisRunsOutOfTime(IRoslynAnalysisService roslynAnalysisService) => roslynAnalysisService .When(x => x.AnalyzeAsync(Arg.Any(), Arg.Any())) - .Do(_ => Task.Delay(milliseconds).GetAwaiter().GetResult()); + .Throw(new OperationCanceledException()); + + private static void SimulateAnalysisWithCallback(IRoslynAnalysisService roslynAnalysisService, Action callback) => + roslynAnalysisService.AnalyzeAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + callback(); + return Task.FromResult(Enumerable.Empty()); + }); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs index b9e0f6be38..b474c92033 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; using System.Net; using System.Text; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.TestInfrastructure; @@ -35,11 +35,14 @@ public class AnalysisRequestHandlerTest private const string ValidToken = "token"; private const string InvalidToken = "wrong"; private const string AnalyzeUrl = "http://localhost/analyze"; + private const string CancelUrl = "http://localhost/cancel"; + private const string UnknownUrl = "http://localhost/SOMERANDOMURL"; private const int DefaultPort = 1234; private const int MaxRequestBodyBytes = 100; private const string AuthTokenHeader = "X-Auth-Token"; private const string HttpMethodPost = "POST"; private const string DiagnosticId = "S100"; + private static readonly Guid AnalysisId = Guid.NewGuid(); private static readonly FileUri FileUri = new("C:\\File.cs"); private IHttpServerSettings settings = null!; private IHttpServerConfigurationProvider configurationProvider = null!; @@ -84,9 +87,10 @@ public void ValidateRequest_RemoteEndpointNull_ReturnsForbidden() { request.RemoteEndPoint.Returns((IPEndPoint?)null); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.Forbidden); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Forbidden); } [TestMethod] @@ -94,9 +98,10 @@ public void ValidateRequest_NotLocalRequest_ReturnsForbidden() { request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.Parse("8.8.8.8"), DefaultPort)); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.Forbidden); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Forbidden); } [TestMethod] @@ -105,9 +110,9 @@ public void ValidateRequest_Loopback_ReturnsOK() MockValidRequest(); request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.Loopback, DefaultPort)); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out _, out _); - result.Should().Be(HttpStatusCode.OK); + result.Should().BeTrue(); } [TestMethod] @@ -116,9 +121,9 @@ public void ValidateRequest_IPv6Loopback_ReturnsOK() MockValidRequest(); request.RemoteEndPoint.Returns(new IPEndPoint(IPAddress.IPv6Loopback, DefaultPort)); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out _, out _); - result.Should().Be(HttpStatusCode.OK); + result.Should().BeTrue(); } [TestMethod] @@ -127,9 +132,10 @@ public void ValidateRequest_TokenInvalid_ReturnsUnauthorized() MockValidRequest(); request.Headers.Returns(new WebHeaderCollection { [AuthTokenHeader] = InvalidToken }); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.Unauthorized); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Unauthorized); } [TestMethod] @@ -141,9 +147,10 @@ public void ValidateRequest_TokenInInvalidHeader_ReturnsUnauthorized(string wron MockValidRequest(); request.Headers.Returns(new WebHeaderCollection { [wrongAuthenticationHeader] = InvalidToken }); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.Unauthorized); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.Unauthorized); } [TestMethod] @@ -152,9 +159,9 @@ public void ValidateRequest_ValidInValidHeader_ReturnsOK() MockValidRequest(); request.Headers.Returns(new WebHeaderCollection { [AuthTokenHeader] = ValidToken }); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out _, out _); - result.Should().Be(HttpStatusCode.OK); + result.Should().BeTrue(); } [TestMethod] @@ -165,9 +172,10 @@ public void ValidateRequest_UrlInvalid_ReturnsNotFound(string invalidUrl) MockValidRequest(); request.Url.Returns(new Uri(invalidUrl)); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.NotFound); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.BadRequest); } [TestMethod] @@ -180,9 +188,10 @@ public void ValidateRequest_MethodInvalid_ReturnsNotFound(string httpMethod) MockValidRequest(); request.HttpMethod.Returns(httpMethod); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.NotFound); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.BadRequest); } [TestMethod] @@ -191,9 +200,10 @@ public void ValidateRequest_ContentLengthExceeded_ReturnsRequestEntityTooLarge() MockValidRequest(); request.ContentLength64.Returns(MaxRequestBodyBytes + 1); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out var errorCode, out _); - result.Should().Be(HttpStatusCode.RequestEntityTooLarge); + result.Should().BeFalse(); + errorCode.Should().Be(HttpStatusCode.RequestEntityTooLarge); logger.Received().LogVerbose(Resources.BodyLengthExceeded, context.Request.ContentLength64, settings.MaxRequestBodyBytes); } @@ -203,9 +213,9 @@ public void ValidateRequest_ContentLengthNotExceeded_ReturnsOK() MockValidRequest(); request.ContentLength64.Returns(MaxRequestBodyBytes); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out _, out _); - result.Should().Be(HttpStatusCode.OK); + result.Should().BeTrue(); } [TestMethod] @@ -213,9 +223,24 @@ public void ValidateRequest_AllValid_ReturnsOK() { MockValidRequest(); - var result = testSubject.ValidateRequest(context); + var result = testSubject.ValidateRequest(context.Request, out _, out _); - result.Should().Be(HttpStatusCode.OK); + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(AnalyzeUrl, RequestType.Analyze)] + [DataRow(CancelUrl, RequestType.Cancel)] + [DataRow(UnknownUrl, RequestType.Unknown)] + public void ValidateRequest_ValidRequestType_SetsCorrectRequestType(string url, RequestType expectedRequestType) + { + MockValidRequest(); + request.Url.Returns(new Uri(url)); + + var result = testSubject.ValidateRequest(context.Request, out _, out var requestType); + + result.Should().Be(expectedRequestType != RequestType.Unknown); + requestType.Should().Be(expectedRequestType); } [TestMethod] @@ -237,7 +262,7 @@ public async Task ParseAnalysisRequestBody_DeserializationFails_ReturnsNull() request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBodyAsync(context); + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); result.Should().BeNull(); } @@ -245,11 +270,11 @@ public async Task ParseAnalysisRequestBody_DeserializationFails_ReturnsNull() [TestMethod] public async Task ParseAnalysisRequestBody_FileNamesMissing_ReturnsNull() { - var stream = new MemoryStream("{\"ActiveRules\":[]}"u8.ToArray()); + var stream = new MemoryStream("""{"ActiveRules":[]}"""u8.ToArray()); request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBodyAsync(context); + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); result.Should().BeNull(); } @@ -257,11 +282,11 @@ public async Task ParseAnalysisRequestBody_FileNamesMissing_ReturnsNull() [TestMethod] public async Task ParseAnalysisRequestBody_FileNamesEmpty_ReturnsNull() { - var stream = new MemoryStream("{\"FileNames\":[],\"ActiveRules\":[]}"u8.ToArray()); + var stream = new MemoryStream("""{"FileNames":[],"ActiveRules":[]}"""u8.ToArray()); request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBodyAsync(context); + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); result.Should().BeNull(); } @@ -269,18 +294,46 @@ public async Task ParseAnalysisRequestBody_FileNamesEmpty_ReturnsNull() [TestMethod] public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel() { - var validRequestJson = $"{{\"FileNames\":[\"{FileUri}\"],\"ActiveRules\":[{{\"RuleId\":\"{DiagnosticId}\"}}]}}"; + var validRequestJson = $$"""{"FileNames":["{{FileUri}}"],"ActiveRules":[{"RuleId":"{{DiagnosticId}}"}], "AnalysisId":"{{AnalysisId}}"}"""; var stream = new MemoryStream(Encoding.UTF8.GetBytes(validRequestJson)); request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); - var result = await testSubject.ParseAnalysisRequestBodyAsync(context); + var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); result.Should().NotBeNull(); result!.FileNames.Should().HaveCount(1); result.FileNames[0].Should().Be(FileUri); result.ActiveRules.Should().HaveCount(1); result.ActiveRules[0].RuleId.Should().Be(DiagnosticId); + result.AnalysisId.Should().Be(AnalysisId); + } + + [TestMethod] + public async Task ParseCancellationRequestBody_DeserializationFails_ReturnsNull() + { + var invalidBodyContent = "{}"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(invalidBodyContent)); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await testSubject.ParseCancellationRequestBodyAsync(context.Request); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task ParseCancellationRequestBody_RequestBodyValid_ReturnsExpectedModel() + { + var validRequestJson = $$"""{"AnalysisId":"{{AnalysisId}}"}"""; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(validRequestJson)); + request.InputStream.Returns(stream); + request.ContentEncoding.Returns(Encoding.UTF8); + + var result = await testSubject.ParseCancellationRequestBodyAsync(context.Request); + + result.Should().NotBeNull(); + result!.AnalysisId.Should().Be(AnalysisId); } private void MockValidRequest() diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs index 2bbbeecaca..1e5b0bd9d6 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/HttpRequestHandlerTest.cs @@ -21,6 +21,7 @@ using System.IO; using System.Net; using System.Text; +using SonarLint.VisualStudio.Integration.TestInfrastructure; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; @@ -32,6 +33,7 @@ public class HttpRequestHandlerTest private IHttpListenerContext context = null!; private IHttpListenerRequest request = null!; private IHttpListenerResponse response = null!; + private Stream stream = null!; private HttpRequestHandler testSubject = null!; [TestInitialize] @@ -42,6 +44,9 @@ public void TestInitialize() context = Substitute.For(); context.Request.Returns(request); context.Response.Returns(response); + stream = Substitute.For(); + stream.CanWrite.Returns(true); + context.Response.OutputStream.Returns(stream); testSubject = new HttpRequestHandler(); } @@ -59,6 +64,17 @@ public void CloseRequest_SetsStatusCodeAndCloses_ReturnsVoid(HttpStatusCode stat response.Received().Close(); } + [TestMethod] + public void CloseRequest_ConnectionBroken_DoesNotWriteResponse() + { + stream.CanWrite.Returns(false); + + testSubject.CloseRequest(context, HttpStatusCode.BadRequest); + + response.DidNotReceiveWithAnyArgs().StatusCode = default; + response.DidNotReceiveWithAnyArgs().Close(); + } + [TestMethod] public async Task SendResponse_WritesCorrectlySerializedDiagnostics() { @@ -81,4 +97,17 @@ public async Task SendResponse_ClosesOutputStream() outputStream.CanRead.Should().BeFalse(); } + + [TestMethod] + public async Task SendResponse_ConnectionBroken_DoesNotWriteResponse() + { + stream.CanWrite.Returns(false); + + await testSubject.SendResponseAsync(context, "{\"Diagnostics\":[}"); + + stream.DidNotReceiveWithAnyArgs().WriteAsync(default, default, default, default).IgnoreAwaitForAssert(); + stream.DidNotReceiveWithAnyArgs().Write(default, default, default); + response.DidNotReceiveWithAnyArgs().StatusCode = default; + response.DidNotReceiveWithAnyArgs().Close(); + } } diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs index 7e5df39089..050c277703 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/RoslynAnalysisHttpServerTest.cs @@ -67,7 +67,7 @@ public void MefCtor_CheckIsExported() => public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); [TestMethod] - public void Ctor_LoggerSetsContext() => _logger.Received(1).ForContext(Resources.HttpServerLogContext).ForContext(nameof(RoslynAnalysisHttpServer)); + public void Ctor_LoggerSetsContext() => _logger.Received(1).ForContext(Resources.RoslynLogContext, Resources.HttpServerLogContext); [TestMethod] public void Dispose_CanBeCalledMultipleTimes() diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs index ec635c5533..f0559db1ff 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -18,7 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Integration.TestInfrastructure; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; @@ -68,16 +70,117 @@ public void MefCtor_CheckIsExported() => public async Task AnalyzeAsync_PassesCorrectArgumentsToEngine() { string[] filePaths = [@"C:\file1.cs", @"C:\folder\file2.cs"]; - analysisConfigurationProvider.GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto).Returns(DefaultAnalysisConfigurations); - analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(Arg.Is(x => x.SequenceEqual(filePaths))).Returns(DefaultProjectAnalysisRequests); + SetUpBasicAnalysisServices(filePaths); analysisEngine.AnalyzeAsync(DefaultProjectAnalysisRequests, DefaultAnalysisConfigurations, Arg.Any()).Returns(DefaultIssues); - var analysisRequest = new AnalysisRequest - { - FileNames = filePaths.Select(x => new FileUri(x)).ToList(), ActiveRules = DefaultActiveRules, AnalysisProperties = DefaultAnalysisProperties, AnalyzerInfo = DefaultAnalyzerInfoDto - }; + + var analysisRequest = CreateAnalysisRequest(filePaths.Select(x => new FileUri(x)).ToList()); var issues = await testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); issues.Should().BeSameAs(DefaultIssues); } + + [TestMethod] + public void Cancel_NonExistingId_ReturnsFalse() + { + var nonExistingId = Guid.NewGuid(); + var cancellationRequest = CreateCancellationRequest(nonExistingId); + + var result = testSubject.Cancel(cancellationRequest); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Cancel_ExistingId_ReturnsTrueAndCancelsToken() + { + var analysisId = Guid.NewGuid(); + var analysisRequest = CreateAnalysisRequest(analysisId: analysisId); + + SetUpBasicAnalysisServices(); + + var taskCompletionSource = new TaskCompletionSource>(); + var internalAnalysisToken = CancellationToken.None; + + analysisEngine.AnalyzeAsync( + Arg.Any>(), + Arg.Any>(), + Arg.Do(t => internalAnalysisToken = t)) + .Returns(taskCompletionSource.Task); + + testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None).IgnoreAwaitForAssert(); + var cancellationRequest = CreateCancellationRequest(analysisId); + + var result = testSubject.Cancel(cancellationRequest); + + result.Should().BeTrue(); + taskCompletionSource.SetResult(DefaultIssues); + internalAnalysisToken.IsCancellationRequested.Should().BeTrue(); + } + + [TestMethod] + public async Task AnalyzeAsync_TokenRemovedAfterAnalysis_EvenIfAnalysisSucceeds() + { + var analysisId = Guid.NewGuid(); + var analysisRequest = CreateAnalysisRequest(analysisId: analysisId); + SetUpBasicAnalysisServices(); + analysisEngine + .AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) + .Returns(DefaultIssues); + + await testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); + + var cancellationRequest = CreateCancellationRequest(analysisId); + var result = testSubject.Cancel(cancellationRequest); + result.Should().BeFalse(); + } + + [TestMethod] + public void AnalyzeAsync_TokenRemovedAfterAnalysis_EvenIfAnalysisThrows() + { + var analysisId = Guid.NewGuid(); + var analysisRequest = CreateAnalysisRequest(analysisId: analysisId); + SetUpBasicAnalysisServices(); + analysisEngine + .AnalyzeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("Test exception")); + + var act = () => testSubject.AnalyzeAsync(analysisRequest, CancellationToken.None); + act.Should().ThrowAsync().IgnoreAwaitForAssert(); + + var cancellationRequest = CreateCancellationRequest(analysisId); + var result = testSubject.Cancel(cancellationRequest); + result.Should().BeFalse(); + } + + private void SetUpConfigurationProvider() => + analysisConfigurationProvider + .GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto) + .Returns(DefaultAnalysisConfigurations); + + private void SetUpBasicAnalysisServices(string[]? filePaths = null) + { + SetUpConfigurationProvider(); + analysisCommandProvider + .GetAnalysisCommandsForCurrentSolution(filePaths is not null ? Arg.Is(x => x.SequenceEqual(filePaths)) : Arg.Any()) + .Returns(DefaultProjectAnalysisRequests); + } + + private static AnalysisRequest CreateAnalysisRequest( + List? fileNames = null, + Guid? analysisId = null) + { + fileNames ??= [new FileUri(@"C:\file1.cs")]; + + return new AnalysisRequest + { + FileNames = fileNames, + ActiveRules = DefaultActiveRules, + AnalysisProperties = DefaultAnalysisProperties, + AnalyzerInfo = DefaultAnalyzerInfoDto, + AnalysisId = analysisId ?? Guid.NewGuid() + }; + } + + private static AnalysisCancellationRequest CreateCancellationRequest(Guid analysisId) => new() { AnalysisId = analysisId }; } diff --git a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs index 4dc3c3f281..039f26862c 100644 --- a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs +++ b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs @@ -30,13 +30,22 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; +public enum RequestType +{ + Unknown, + Analyze, + Cancel +} + public interface IAnalysisRequestHandler { - Task ParseAnalysisRequestBodyAsync(IHttpListenerContext context); + Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request); - string ParseAnalysisRequestResponse(List diagnostics); + Task ParseCancellationRequestBodyAsync(IHttpListenerRequest request); - HttpStatusCode ValidateRequest(IHttpListenerContext context); + string SerializeAnalysisRequestResponse(List diagnostics); + + bool ValidateRequest(IHttpListenerRequest request, out HttpStatusCode errorCode, out RequestType requestType); } [Export(typeof(IAnalysisRequestHandler))] @@ -46,60 +55,53 @@ internal class AnalysisRequestHandler(ILogger logger, IHttpServerSettings server { private const string XAuthTokenHeader = "X-Auth-Token"; private const string AnalyzeRequestUrl = "/analyze"; + private const string CancelAnalysisRequestUrl = "/cancel"; private readonly ILogger logger = logger.ForContext(Resources.HttpServerLogContext).ForContext(nameof(AnalysisRequestHandler)); - public HttpStatusCode ValidateRequest(IHttpListenerContext context) + public string SerializeAnalysisRequestResponse(List diagnostics) { - if (VerifyLocalRequest(context) is var localRequestStatusCode && localRequestStatusCode != HttpStatusCode.OK) - { - return localRequestStatusCode; - } - if (VerifyToken(context) is var tokenStatusCode && tokenStatusCode != HttpStatusCode.OK) - { - return tokenStatusCode; - } - if (VerifyMethod(context) is var methodStatusCode && methodStatusCode != HttpStatusCode.OK) - { - return methodStatusCode; - } - if (VerifyContentLength(context) is var contentLengthStatusCode && contentLengthStatusCode != HttpStatusCode.OK) - { - return contentLengthStatusCode; - } - return HttpStatusCode.OK; + var responseObj = new AnalysisResponse { RoslynIssues = diagnostics }; + var responseString = JsonConvert.SerializeObject(responseObj); + return responseString; } - public async Task ParseAnalysisRequestBodyAsync(IHttpListenerContext context) + public bool ValidateRequest(IHttpListenerRequest request, out HttpStatusCode errorCode, out RequestType requestType) { - var body = await ReadBodyAsync(context); - var requestDto = GetAnalysisRequestFromBody(body); - if (requestDto != null && requestDto.FileNames.Count != 0) - { - return requestDto; - } + requestType = RequestType.Unknown; + errorCode = default; + return VerifyLocalRequest(request, out errorCode) + && VerifyToken(request, out errorCode) + && VerifyMethod(request, out errorCode, out requestType) + && VerifyContentLength(request, out errorCode); + } - return null; + public async Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request) + { + var analysisRequestBodyAsync = await ParseAnalysisRequestBodyAsync(request); + return analysisRequestBodyAsync is { FileNames.Count: > 0, ActiveRules.Count: > 0 } ? analysisRequestBodyAsync : null; } - public string ParseAnalysisRequestResponse(List diagnostics) + public Task ParseCancellationRequestBodyAsync(IHttpListenerRequest request) => + ParseAnalysisRequestBodyAsync(request); + + internal static async Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request) where T : class { - var responseObj = new AnalysisResponse { RoslynIssues = diagnostics }; - var responseString = JsonConvert.SerializeObject(responseObj); - return responseString; + var body = await ReadBodyAsync(request); + return GetAnalysisRequestFromBody(body); } - private static async Task ReadBodyAsync(IHttpListenerContext context) + private static async Task ReadBodyAsync(IHttpListenerRequest request) { - using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding); + using var reader = new StreamReader(request.InputStream, request.ContentEncoding); return await reader.ReadToEndAsync(); } - private static AnalysisRequest? GetAnalysisRequestFromBody(string body) + private static T? GetAnalysisRequestFromBody(string body) where T : class { - AnalysisRequest? requestDto; + T? requestDto; try { - requestDto = JsonConvert.DeserializeObject(body); + requestDto = JsonConvert.DeserializeObject(body); } catch (Exception) { @@ -108,37 +110,66 @@ private static async Task ReadBodyAsync(IHttpListenerContext context) return requestDto; } - private static HttpStatusCode VerifyLocalRequest(IHttpListenerContext context) => IsLocalRequest(context.Request) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; - - private static bool IsLocalRequest(IHttpListenerRequest request) + private static bool VerifyLocalRequest(IHttpListenerRequest request, out HttpStatusCode errorCode) { - var remote = request.RemoteEndPoint; - return remote != null && (remote.Address.Equals(IPAddress.Loopback) || remote.Address.Equals(IPAddress.IPv6Loopback)); + if (!IsLocalRequest(request)) + { + errorCode = HttpStatusCode.Forbidden; + return false; + } + errorCode = default; + return true; } - private HttpStatusCode VerifyToken(IHttpListenerContext context) + private bool VerifyToken(IHttpListenerRequest request, out HttpStatusCode errorCode) { - var token = context.Request.Headers[XAuthTokenHeader]; - - return token == serverConfigurationProvider.CurrentConfiguration.Token.ToUnsecureString() ? HttpStatusCode.OK : HttpStatusCode.Unauthorized; + var token = request.Headers[XAuthTokenHeader]; + if (token != serverConfigurationProvider.CurrentConfiguration.Token.ToUnsecureString()) + { + errorCode = HttpStatusCode.Unauthorized; + return false; + } + errorCode = default; + return true; } - private static HttpStatusCode VerifyMethod(IHttpListenerContext context) + private static bool VerifyMethod(IHttpListenerRequest request, out HttpStatusCode errorCode, out RequestType requestType) { - if (context.Request.HttpMethod == HttpMethod.Post.Method && context.Request.Url.AbsolutePath == AnalyzeRequestUrl) + errorCode = default; + if (request.HttpMethod == HttpMethod.Post.Method && request.Url.AbsolutePath == AnalyzeRequestUrl) { - return HttpStatusCode.OK; + requestType = RequestType.Analyze; + return true; } - return HttpStatusCode.NotFound; + + if (request.HttpMethod == HttpMethod.Post.Method && request.Url.AbsolutePath == CancelAnalysisRequestUrl) + { + requestType = RequestType.Cancel; + return true; + } + + requestType = RequestType.Unknown; + errorCode = HttpStatusCode.BadRequest; + return false; } - private HttpStatusCode VerifyContentLength(IHttpListenerContext context) + private bool VerifyContentLength(IHttpListenerRequest request, out HttpStatusCode errorCode) { - if (context.Request.ContentLength64 <= serverSettings.MaxRequestBodyBytes) + if (request.ContentLength64 > serverSettings.MaxRequestBodyBytes) { - return HttpStatusCode.OK; + logger.LogVerbose(Resources.BodyLengthExceeded, request.ContentLength64, serverSettings.MaxRequestBodyBytes); + errorCode = HttpStatusCode.RequestEntityTooLarge; + return false; } - logger.LogVerbose(Resources.BodyLengthExceeded, context.Request.ContentLength64, serverSettings.MaxRequestBodyBytes); - return HttpStatusCode.RequestEntityTooLarge; + errorCode = default; + return true; + } + + private static bool IsLocalRequest(IHttpListenerRequest request) + { + var remoteAddress = request.RemoteEndPoint?.Address; + return remoteAddress != null + && (remoteAddress.Equals(IPAddress.Loopback) + || remoteAddress.Equals(IPAddress.IPv6Loopback)); } } diff --git a/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs b/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs index 81603b82f1..bd363c08ff 100644 --- a/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs +++ b/src/RoslynAnalyzerServer/Http/HttpRequestHandler.cs @@ -39,6 +39,11 @@ internal class HttpRequestHandler() : IHttpRequestHandler { public void CloseRequest(IHttpListenerContext context, HttpStatusCode statusCode) { + if (!context.Response.OutputStream.CanWrite) + { + return; + } + context.Response.StatusCode = (int)statusCode; context.Response.Close(); } @@ -47,6 +52,11 @@ public void CloseRequest(IHttpListenerContext context, HttpStatusCode statusCode private static async Task WriteResponseAsync(string responseString, IHttpListenerContext context, HttpStatusCode statusCode) { + if (!context.Response.OutputStream.CanWrite) + { + return; + } + var buffer = Encoding.UTF8.GetBytes(responseString); context.Response.ContentLength64 = buffer.Length; context.Response.StatusCode = (int)statusCode; diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs new file mode 100644 index 0000000000..847288ac40 --- /dev/null +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisCancellationRequest.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; + +public record AnalysisCancellationRequest +{ + [JsonRequired] + public Guid AnalysisId { get; set; } +} diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs index 0e17d10035..e1ce7cb867 100644 --- a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs @@ -18,14 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using Newtonsoft.Json; using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; public record AnalysisRequest { + [JsonRequired] public List FileNames { get; set; } = []; + [JsonRequired] public List ActiveRules { get; set; } = []; public Dictionary AnalysisProperties { get; set; } = []; public AnalyzerInfoDto AnalyzerInfo { get; set; } = null!; + [JsonRequired] + public Guid AnalysisId { get; set; } } diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs index 339959c1d6..ceca6d7b73 100644 --- a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -22,6 +22,7 @@ using System.Net; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Adapters; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http; @@ -38,7 +39,7 @@ internal sealed class RoslynAnalysisHttpServer( IRoslynAnalysisService roslynAnalysisService) : IRoslynAnalysisHttpServer { private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly ILogger logger = logger.ForContext(Resources.HttpServerLogContext).ForContext(nameof(RoslynAnalysisHttpServer)); + private readonly ILogger logger = logger.ForContext(Resources.RoslynLogContext, Resources.HttpServerLogContext); private HttpListener? httpListener; private bool isDisposed; @@ -129,37 +130,64 @@ private async Task HandleRequestWithTimeoutAsync(IHttpListenerContext context, C { using var requestCancellationToken = new CancellationTokenSource(settings.RequestMillisecondsTimeout); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(serverCancellationToken, requestCancellationToken.Token); + + var requestContext = new MessageLevelContext() { VerboseContext = [ $"Request {Guid.NewGuid().ToString()}"] }; + try { - await Task.Run(() => HandleRequestAsync(context, linkedCts.Token), linkedCts.Token); + logger.LogVerbose(requestContext, "Received request {0}", context.Request.Url); + var statusCode = await Task.Run(() => HandleRequestAsync(context, linkedCts.Token), linkedCts.Token); + logger.LogVerbose(requestContext, "Response code {0}", statusCode); } catch (OperationCanceledException) { - logger.LogVerbose(Resources.HttpRequestTimedOut, settings.RequestMillisecondsTimeout); + logger.LogVerbose(requestContext, Resources.HttpRequestTimedOut, settings.RequestMillisecondsTimeout); httpRequestHandler.CloseRequest(context, HttpStatusCode.RequestTimeout); } catch (Exception exception) { - logger.LogVerbose(Resources.HttpRequestFailed, exception.Message + exception.StackTrace); + logger.LogVerbose(requestContext, Resources.HttpRequestFailed, exception.Message + exception.StackTrace); httpRequestHandler.CloseRequest(context, HttpStatusCode.InternalServerError); } } - private async Task HandleRequestAsync(IHttpListenerContext context, CancellationToken cancellationToken) + private async Task HandleRequestAsync(IHttpListenerContext context, CancellationToken cancellationToken) { - if (analysisRequestHandler.ValidateRequest(context) is var validationStatusCode && validationStatusCode != HttpStatusCode.OK) + if (!analysisRequestHandler.ValidateRequest(context.Request, out var validationStatusCode, out var requestType)) { httpRequestHandler.CloseRequest(context, validationStatusCode); - return; + return validationStatusCode; } - if (await analysisRequestHandler.ParseAnalysisRequestBodyAsync(context) is not { } analysisRequest) + + return requestType switch { - httpRequestHandler.CloseRequest(context, HttpStatusCode.BadRequest); - return; - } + RequestType.Analyze when await analysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request) is { } analysisRequest => await HandleAnalyzeAsync(context, cancellationToken, analysisRequest), + RequestType.Cancel when await analysisRequestHandler.ParseCancellationRequestBodyAsync(context.Request) is { } cancellationRequest => HandleCancel(context, cancellationRequest), + _ => HandleBadRequest(context) + }; + } + private HttpStatusCode HandleBadRequest(IHttpListenerContext context) + { + httpRequestHandler.CloseRequest(context, HttpStatusCode.BadRequest); + return HttpStatusCode.BadRequest; + } + + private HttpStatusCode HandleCancel(IHttpListenerContext context, AnalysisCancellationRequest cancellationRequest) + { + var status = roslynAnalysisService.Cancel(cancellationRequest); + var httpStatusCode = status ? HttpStatusCode.OK : HttpStatusCode.NotFound; + httpRequestHandler.CloseRequest(context, httpStatusCode); + return httpStatusCode; + } + + private async Task HandleAnalyzeAsync( + IHttpListenerContext context, + CancellationToken cancellationToken, + AnalysisRequest analysisRequest) + { var issues = await roslynAnalysisService.AnalyzeAsync(analysisRequest, cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); - await httpRequestHandler.SendResponseAsync(context, analysisRequestHandler.ParseAnalysisRequestResponse(issues.ToList())); + await httpRequestHandler.SendResponseAsync(context, analysisRequestHandler.SerializeAnalysisRequestResponse(issues.ToList())); + return HttpStatusCode.OK; } } diff --git a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs index 6e076a54d3..b460dc0b33 100644 --- a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs @@ -26,4 +26,5 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer; internal interface IRoslynAnalysisService { Task> AnalyzeAsync(AnalysisRequest analysisRequest, CancellationToken cancellationToken); + bool Cancel(AnalysisCancellationRequest analysisCancellationRequest); } diff --git a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs index 24a568be94..b3e248db47 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -33,11 +33,58 @@ internal class RoslynAnalysisService( IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService { + private readonly object locker = new(); + private readonly Dictionary cancellationTokensForAnalysis = new(); + public async Task> AnalyzeAsync( AnalysisRequest analysisRequest, - CancellationToken cancellationToken) => - await analysisEngine.AnalyzeAsync( - analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileNames.Select(x => x.LocalPath).ToArray()), - await analysisConfigurationProvider.GetConfigurationAsync(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), - cancellationToken); + CancellationToken cancellationToken) + { + try + { + return await analysisEngine.AnalyzeAsync( + analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileNames.Select(x => x.LocalPath).ToArray()), + await analysisConfigurationProvider.GetConfigurationAsync(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), + SetUpCancellationTokenForAnalysis(analysisRequest, cancellationToken)); + } + finally + { + CancelAndCleanUpToken(analysisRequest.AnalysisId); + } + } + + public bool Cancel(AnalysisCancellationRequest analysisCancellationRequest) + { + return CancelAndCleanUpToken(analysisCancellationRequest.AnalysisId); + } + + private CancellationToken SetUpCancellationTokenForAnalysis( + AnalysisRequest analysisRequest, + CancellationToken cancellationToken) + { + lock (locker) + { + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cancellationTokensForAnalysis[analysisRequest.AnalysisId] = cancellationTokenSource; + cancellationToken = cancellationTokenSource.Token; + } + return cancellationToken; + } + + private bool CancelAndCleanUpToken(Guid analysisId) + { + CancellationTokenSource? cancellationTokenSource; + lock (locker) + { + if (!cancellationTokensForAnalysis.TryGetValue(analysisId, out cancellationTokenSource)) + { + return false; + } + cancellationTokensForAnalysis.Remove(analysisId); + } + + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + return true; + } } From 6cef7411c5abadd071ed2ba6b5db415a88a05d54 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:51:33 +0200 Subject: [PATCH 34/38] SLVS-2513 Reanalyze open files on significant roslyn workspace changes (#6408) [SLVS-2513](https://sonarsource.atlassian.net/browse/SLVS-2513) Part of [SLVS-2513]: https://sonarsource.atlassian.net/browse/SLVS-2513?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../SonarLintTagger/TaggerProviderTests.cs | 6 ++ .../TextBufferIssueTrackerTests.cs | 5 +- .../SonarLintTagger/TaggerProvider.cs | 9 +- .../Wrappers/WorkspaceChangeIndicatorTests.cs | 51 +++++++++++ .../RoslynAnalysisServiceTests.cs | 13 ++- .../Wrappers/IRoslynWorkspaceWrapper.cs | 2 +- .../Wrappers/IWorkspaceChangeIndicator.cs | 44 ++++++++++ .../Wrappers/RoslynWorkspaceWrapper.cs | 62 ++++++++++++- .../Wrappers/WorkspaceChangeIndicator.cs | 87 +++++++++++++++++++ .../ApplyChangesOperation.Roslyn.cs | 28 +----- .../Http/RoslynAnalysisHttpServer.cs | 1 + .../IRoslynAnalysisService.cs | 2 +- .../RoslynAnalysisService.cs | 6 +- 13 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs create mode 100644 src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs index 5264f5dae6..2c6141ff4e 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TaggerProviderTests.cs @@ -59,6 +59,7 @@ public class TaggerProviderTests private IInitializationProcessorFactory initializationProcessorFactory; private IThreadHandling threadHandling; private ITaskExecutorWithDebounceFactory taskExecutorWithDebounceFactory; + private ITaskExecutorWithDebounce reanalysisExecutorWithDebounce; private static readonly AnalysisLanguage[] DetectedLanguagesJsTs = [AnalysisLanguage.TypeScript, AnalysisLanguage.Javascript]; @@ -93,6 +94,9 @@ public void SetUp() threadHandling = Substitute.ForPartsOf(); taskExecutorWithDebounceFactory = Substitute.For(); + reanalysisExecutorWithDebounce = Substitute.For(); + taskExecutorWithDebounceFactory.Create(Arg.Any()).Returns(reanalysisExecutorWithDebounce); + reanalysisExecutorWithDebounce.When(x => x.Debounce(Arg.Any())).Do(call => call.Arg().Invoke()); testSubject = CreateAndInitializeTestSubject(); } @@ -146,6 +150,7 @@ public void Ctor_InitializesInCorrectOrder() => [TestMethod] public void CreateTagger_should_create_tracker_when_tagger_is_created() { + taskExecutorWithDebounceFactory.ClearReceivedCalls(); var doc = CreateMockedDocument("anyname"); var tagger = CreateTaggerForDocument(doc); @@ -501,6 +506,7 @@ public void AnalysisRequested_CallsAnalyzerRequestAnalysis() mockAnalysisRequester.AnalysisRequested += Raise.EventWith(this, new AnalysisRequestEventArgs(filesToaAnalyze)); analysisExecutingSignal.WaitOne(AnalysisTimeout); + reanalysisExecutorWithDebounce.Received(1).Debounce(Arg.Any()); analyzer.Received(1).ExecuteAnalysis(Arg.Is>(x => x.SequenceEqual(filesToaAnalyze))); } diff --git a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs index 60eae8e849..3c85faf7b3 100644 --- a/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs +++ b/src/Integration.Vsix.UnitTests/SonarLintTagger/TextBufferIssueTrackerTests.cs @@ -68,14 +68,15 @@ public void SetUp() issueConsumerFactory = Substitute.For(); issueConsumerStorage = Substitute.For(); issueConsumer = Substitute.For(); + taskExecutorWithDebounceFactory = Substitute.For(); + taskExecutorWithDebounce = Substitute.For(); taggerProvider = CreateTaggerProvider(); mockTextSnapshot = CreateTextSnapshotMock(); mockDocumentTextBuffer = CreateTextBufferMock(mockTextSnapshot); mockedJavascriptDocumentFooJs = CreateDocumentMock("foo.js", mockDocumentTextBuffer); javascriptLanguage = [AnalysisLanguage.Javascript]; - taskExecutorWithDebounceFactory = Substitute.For(); - taskExecutorWithDebounce = Substitute.For(); taskExecutorWithDebounceFactory.Create(Arg.Any()).Returns(taskExecutorWithDebounce); + MockIssueConsumerFactory(mockedJavascriptDocumentFooJs, issueConsumer); testSubject = CreateTestSubject(mockedJavascriptDocumentFooJs); diff --git a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs index 869646d8ff..37f438076e 100644 --- a/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs +++ b/src/Integration.Vsix/SonarLintTagger/TaggerProvider.cs @@ -79,6 +79,7 @@ internal sealed class TaggerProvider : ITaggerProvider, IRequireInitialization, private CancellableJobRunner reanalysisJob; private StatusBarReanalysisProgressHandler reanalysisProgressHandler; private IVsStatusbar vsStatusBar; + private readonly ITaskExecutorWithDebounce requestAnalysisDebounceExecutor; internal IEnumerable ActiveTrackersForTesting => issueTrackers; @@ -111,6 +112,7 @@ internal TaggerProvider( this.analyzer = analyzer; this.logger = logger; this.taskExecutorWithDebounceFactory = taskExecutorWithDebounceFactory; + requestAnalysisDebounceExecutor = taskExecutorWithDebounceFactory.Create(debounceMilliseconds); InitializationProcessor = initializationProcessorFactory.CreateAndStart( [], @@ -138,10 +140,11 @@ public void Dispose() public IInitializationProcessor InitializationProcessor { get; } - private void OnAnalysisRequested(object sender, AnalysisRequestEventArgs args) - { - // This method is not currently used, but is left here as there are opportunities to use it in the future + private void OnAnalysisRequested(object sender, AnalysisRequestEventArgs args) => + requestAnalysisDebounceExecutor.Debounce(() => RequestAnalysis(args)); + private void RequestAnalysis(AnalysisRequestEventArgs args) + { lock (reanalysisLockObject) { reanalysisJob?.Cancel(); diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs new file mode 100644 index 0000000000..e1e9c8208d --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/Wrappers/WorkspaceChangeIndicatorTests.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis.Wrappers; + +[TestClass] +public class WorkspaceChangeIndicatorTests +{ + [DataTestMethod] + [DataRow(WorkspaceChangeKind.DocumentAdded, true)] + [DataRow(WorkspaceChangeKind.DocumentRemoved, true)] + [DataRow(WorkspaceChangeKind.DocumentReloaded, true)] + [DataRow(WorkspaceChangeKind.DocumentChanged, true)] + [DataRow(WorkspaceChangeKind.DocumentInfoChanged, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentAdded, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentRemoved, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentReloaded, true)] + [DataRow(WorkspaceChangeKind.AdditionalDocumentChanged, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentAdded, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentRemoved, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentReloaded, true)] + [DataRow(WorkspaceChangeKind.AnalyzerConfigDocumentChanged, true)] + [DataRow(WorkspaceChangeKind.SolutionAdded, false)] + [DataRow(WorkspaceChangeKind.SolutionRemoved, false)] + [DataRow(WorkspaceChangeKind.SolutionChanged, false)] + [DataRow(WorkspaceChangeKind.ProjectAdded, false)] + [DataRow(WorkspaceChangeKind.ProjectRemoved, false)] + [DataRow(WorkspaceChangeKind.ProjectChanged, false)] + public void IsChangeKindTrivial_ReturnsExpectedResult(WorkspaceChangeKind kind, bool isTrivial) => + new WorkspaceChangeIndicator().IsChangeKindTrivial(kind).Should().Be(isTrivial); +} diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs index f0559db1ff..f5fc32f550 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -44,21 +44,24 @@ public class RoslynAnalysisServiceTests private IRoslynAnalysisConfigurationProvider analysisConfigurationProvider = null!; private IRoslynAnalysisEngine analysisEngine = null!; + private IRoslynWorkspaceWrapper workspace = null!; private RoslynAnalysisService testSubject = null!; [TestInitialize] public void TestInitialize() { + workspace = Substitute.For(); analysisEngine = Substitute.For(); analysisConfigurationProvider = Substitute.For(); analysisCommandProvider = Substitute.For(); - testSubject = new RoslynAnalysisService(analysisEngine, analysisConfigurationProvider, analysisCommandProvider); + testSubject = new RoslynAnalysisService(workspace, analysisEngine, analysisConfigurationProvider, analysisCommandProvider); } [TestMethod] public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -153,6 +156,14 @@ public void AnalyzeAsync_TokenRemovedAfterAnalysis_EvenIfAnalysisThrows() result.Should().BeFalse(); } + [TestMethod] + public void Dispose_DisposesWorkspace() + { + testSubject.Dispose(); + + workspace.Received().Dispose(); + } + private void SetUpConfigurationProvider() => analysisConfigurationProvider .GetConfigurationAsync(DefaultActiveRules, DefaultAnalysisProperties, DefaultAnalyzerInfoDto) diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs index efad6b1d3d..9c0682aa25 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IRoslynWorkspaceWrapper.cs @@ -23,7 +23,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; -internal interface IRoslynWorkspaceWrapper +internal interface IRoslynWorkspaceWrapper : IDisposable { IRoslynSolutionWrapper GetCurrentSolution(); diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs new file mode 100644 index 0000000000..f3af6edc53 --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/IWorkspaceChangeIndicator.cs @@ -0,0 +1,44 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +/// +/// Indicates whether workspace changes are critical and require reanalysis +/// +internal interface IWorkspaceChangeIndicator +{ + /// + /// Checks if solution change event is not critical. Does not guarantee that it is critical if returns false. + /// + bool IsChangeKindTrivial(WorkspaceChangeKind kind); + + /// + /// Checks if solution changes are critical and require reanalysis + /// + bool SolutionChangedCritically(SolutionChanges solutionChanges); + + /// + /// Checks if project changes are critical and require reanalysis + /// + bool ProjectChangedCritically(ProjectChanges changedProject); +} diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs index 968a5c421c..8c02b76ad6 100644 --- a/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/RoslynWorkspaceWrapper.cs @@ -23,20 +23,74 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Analysis; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; [ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace [Export(typeof(IRoslynWorkspaceWrapper))] [PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class RoslynWorkspaceWrapper([Import(typeof(VisualStudioWorkspace))] Workspace workspace, ILogger logger) : IRoslynWorkspaceWrapper +internal sealed class RoslynWorkspaceWrapper : IRoslynWorkspaceWrapper { - private readonly ILogger quickFixApplicationLogger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynQuickFixLogContext); + private readonly ILogger quickFixApplicationLogger; + private readonly Workspace workspace; + private readonly IAnalysisRequester analysisRequester; + private readonly IThreadHandling threadHandling; + private readonly IWorkspaceChangeIndicator workspaceChangeIndicator; + private bool disposed; + + [method: ImportingConstructor] + public RoslynWorkspaceWrapper( + [Import(typeof(VisualStudioWorkspace))] + Workspace workspace, + IWorkspaceChangeIndicator workspaceChangeIndicator, + IAnalysisRequester analysisRequester, + ILogger logger, + IThreadHandling threadHandling) + { + this.workspace = workspace; + this.analysisRequester = analysisRequester; + this.threadHandling = threadHandling; + this.workspaceChangeIndicator = workspaceChangeIndicator; + workspace.WorkspaceChanged += WorkspaceOnWorkspaceChanged; + quickFixApplicationLogger = logger.ForContext(Resources.RoslynLogContext, Resources.RoslynQuickFixLogContext); + } public IRoslynSolutionWrapper GetCurrentSolution() => new RoslynSolutionWrapper(workspace.CurrentSolution); public Task ApplyOrMergeChangesAsync(IRoslynSolutionWrapper originalSolution, Microsoft.CodeAnalysis.CodeActions.ApplyChangesOperation operation, CancellationToken cancellationToken) => - ApplyChangesOperation.ApplyOrMergeChangesAsync(workspace, originalSolution.RoslynSolution, operation.ChangedSolution, quickFixApplicationLogger, cancellationToken); + ApplyChangesOperation.ApplyOrMergeChangesAsync(workspace, originalSolution.RoslynSolution, operation.ChangedSolution, quickFixApplicationLogger, workspaceChangeIndicator, cancellationToken); + + // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace + private void WorkspaceOnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) + { + if (workspaceChangeIndicator.IsChangeKindTrivial(e.Kind)) + { + return; + } + + threadHandling.RunOnBackgroundThread(() => + { + var solutionChanges = e.NewSolution.GetChanges(e.OldSolution); + + if (workspaceChangeIndicator.SolutionChangedCritically(solutionChanges) + || solutionChanges.GetProjectChanges().Any(changedProject => workspaceChangeIndicator.ProjectChangedCritically(changedProject))) + { + analysisRequester.RequestAnalysis(); + } + }).Forget(); + } + + public void Dispose() + { + if (disposed) + { + return; + } + + workspace.WorkspaceChanged -= WorkspaceOnWorkspaceChanged; + disposed = true; + } } diff --git a/src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs b/src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs new file mode 100644 index 0000000000..242fe9557d --- /dev/null +++ b/src/RoslynAnalyzerServer/Analysis/Wrappers/WorkspaceChangeIndicator.cs @@ -0,0 +1,87 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using System.Linq; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; + +[ExcludeFromCodeCoverage] // todo SLVS-2466 add roslyn 'integration' tests using AdHocWorkspace +[Export(typeof(IWorkspaceChangeIndicator))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class WorkspaceChangeIndicator : IWorkspaceChangeIndicator +{ + private readonly HashSet trivialChanges = + [ + // Currently changes in other files should not affect too many quickfixes + // There is an opportunity for improvement: + // this group needs to be removed, and a mechanism that either re-analyzes on apply fail, + // or a background analysis job, need to be added + // todo https://sonarsource.atlassian.net/browse/SLVS-2612 + WorkspaceChangeKind.DocumentAdded, + WorkspaceChangeKind.DocumentRemoved, + WorkspaceChangeKind.DocumentReloaded, + WorkspaceChangeKind.DocumentChanged, + WorkspaceChangeKind.DocumentInfoChanged, + WorkspaceChangeKind.AdditionalDocumentAdded, + WorkspaceChangeKind.AdditionalDocumentRemoved, + WorkspaceChangeKind.AdditionalDocumentReloaded, + WorkspaceChangeKind.AdditionalDocumentChanged, + + // we don't care about other analyzers + WorkspaceChangeKind.AnalyzerConfigDocumentAdded, + WorkspaceChangeKind.AnalyzerConfigDocumentRemoved, + WorkspaceChangeKind.AnalyzerConfigDocumentReloaded, + WorkspaceChangeKind.AnalyzerConfigDocumentChanged, + ]; + + public bool IsChangeKindTrivial(WorkspaceChangeKind kind) => trivialChanges.Contains(kind); + + public bool SolutionChangedCritically(SolutionChanges solutionChanges) => + solutionChanges.GetAddedProjects().Any() || + solutionChanges.GetRemovedProjects().Any(); + + // We don't care about other analyzers. This is preserved for documentation's sake, as this code was originally copied from roslyn's github + // solutionChanges.GetAddedAnalyzerReferences().Any() || + // solutionChanges.GetRemovedAnalyzerReferences().Any(); + + public bool ProjectChangedCritically(ProjectChanges changedProject) => + changedProject.GetAddedMetadataReferences().Any() || + changedProject.GetAddedProjectReferences().Any() || + changedProject.GetRemovedMetadataReferences().Any() || + changedProject.GetRemovedProjectReferences().Any(); + + // todo https://sonarsource.atlassian.net/browse/SLVS-2612 + // Currently changes in other files should not affect too many quickfixes, see comment above. + // changedProject.GetAddedDocuments().Any() || + // changedProject.GetRemovedDocuments().Any() || + // changedProject.GetAddedAdditionalDocuments().Any() || + // changedProject.GetRemovedAdditionalDocuments().Any() || + + // We don't care about configs as we get rules configuration from SLCore. This is preserved for documentation's sake, as this code was originally copied from roslyn's github + // changedProject.GetAddedAnalyzerConfigDocuments().Any() || + // changedProject.GetRemovedAnalyzerConfigDocuments().Any() || + + // We don't care about other analyzers or suppressors. This is preserved for documentation's sake, as this code was originally copied from roslyn's github + // changedProject.GetAddedAnalyzerReferences().Any() || + // changedProject.GetRemovedAnalyzerReferences().Any() || +} diff --git a/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs index 45ddc08a90..ed7871a57b 100644 --- a/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs +++ b/src/RoslynAnalyzerServer/ApplyChangesOperation.Roslyn.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; using Document = Microsoft.CodeAnalysis.Document; namespace SonarLint.VisualStudio.RoslynAnalyzerServer; @@ -19,6 +20,7 @@ internal static async Task ApplyOrMergeChangesAsync( Solution originalSolution, Solution changedSolution, ILogger logger, + IWorkspaceChangeIndicator workspaceChangeIndicator, CancellationToken cancellationToken) { var currentSolution = workspace.CurrentSolution; @@ -41,12 +43,10 @@ internal static async Task ApplyOrMergeChangesAsync( var solutionChanges = changedSolution.GetChanges(originalSolution); - if (SolutionChangedCritically(solutionChanges)) + if (workspaceChangeIndicator.SolutionChangedCritically(solutionChanges)) { logger.LogVerbose(Resources.ApplyChangesOperation_SolutionChanged); return false; - // todo https://sonarsource.atlassian.net/browse/SLVS-2513 this will lead to invalid quickfixes if project configuration changes. - // do we need to reanalyze open files on major workspace changes? can modified analyzer references be ignored? } // Take the actual current solution the workspace is pointing to and fork it with just the text changes the @@ -56,7 +56,7 @@ internal static async Task ApplyOrMergeChangesAsync( foreach (var changedProject in solutionChanges.GetProjectChanges()) { // We only support text changes. If we see any other changes to this project, bail out immediately. - if (ProjectChangedCritically(changedProject)) + if (workspaceChangeIndicator.ProjectChangedCritically(changedProject)) { logger.LogVerbose(Resources.ApplyChangesOperation_ProjectChanged, changedProject.NewProject.Name); return false; @@ -136,24 +136,4 @@ private static bool GetDocuments( } return true; } - - private static bool SolutionChangedCritically(SolutionChanges solutionChanges) => - solutionChanges.GetAddedProjects().Any() || - solutionChanges.GetAddedAnalyzerReferences().Any() || - solutionChanges.GetRemovedProjects().Any() || - solutionChanges.GetRemovedAnalyzerReferences().Any(); - - private static bool ProjectChangedCritically(ProjectChanges changedProject) => - changedProject.GetAddedAdditionalDocuments().Any() || - changedProject.GetAddedAnalyzerConfigDocuments().Any() || - changedProject.GetAddedAnalyzerReferences().Any() || - changedProject.GetAddedDocuments().Any() || - changedProject.GetAddedMetadataReferences().Any() || - changedProject.GetAddedProjectReferences().Any() || - changedProject.GetRemovedAdditionalDocuments().Any() || - changedProject.GetRemovedAnalyzerConfigDocuments().Any() || - changedProject.GetRemovedAnalyzerReferences().Any() || - changedProject.GetRemovedDocuments().Any() || - changedProject.GetRemovedMetadataReferences().Any() || - changedProject.GetRemovedProjectReferences().Any(); } diff --git a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs index ceca6d7b73..3054cedaf3 100644 --- a/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs +++ b/src/RoslynAnalyzerServer/Http/RoslynAnalysisHttpServer.cs @@ -81,6 +81,7 @@ public void Dispose() cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); httpListener?.Close(); + roslynAnalysisService.Dispose(); isDisposed = true; logger.LogVerbose(Resources.HttpServerDisposed); } diff --git a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs index b460dc0b33..17b1e7c3d3 100644 --- a/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/IRoslynAnalysisService.cs @@ -23,7 +23,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer; -internal interface IRoslynAnalysisService +internal interface IRoslynAnalysisService : IDisposable { Task> AnalyzeAsync(AnalysisRequest analysisRequest, CancellationToken cancellationToken); bool Cancel(AnalysisCancellationRequest analysisCancellationRequest); diff --git a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs index b3e248db47..0f9b9f852c 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer; @@ -28,7 +29,8 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer; [Export(typeof(IRoslynAnalysisService))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] -internal class RoslynAnalysisService( +internal sealed class RoslynAnalysisService( + IRoslynWorkspaceWrapper workspaceWrapper, IRoslynAnalysisEngine analysisEngine, IRoslynAnalysisConfigurationProvider analysisConfigurationProvider, IRoslynSolutionAnalysisCommandProvider analysisCommandProvider) : IRoslynAnalysisService @@ -87,4 +89,6 @@ private bool CancelAndCleanUpToken(Guid analysisId) cancellationTokenSource.Dispose(); return true; } + + public void Dispose() => workspaceWrapper.Dispose(); } From 336d2f1d983e3240ebdb2a38e1a401befad59709 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:25:40 +0200 Subject: [PATCH 35/38] SLVS-2611 Adapt roslyn analysis server dtos to support FileUri (#6461) [SLVS-2611](https://sonarsource.atlassian.net/browse/SLVS-2611) [SLVS-2611]: https://sonarsource.atlassian.net/browse/SLVS-2611?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- src/EmbeddedSonarAnalyzer.props | 2 +- .../Http/RoslynAnalysisHttpServerTest.cs | 2 +- .../DiagnosticDuplicatesComparerTests.cs | 29 +++--- .../DiagnosticToRoslynIssueConverterTests.cs | 26 +++--- .../SequentialRoslynAnalysisEngineTests.cs | 9 +- .../Http/AnalysisRequestHandlerTest.cs | 6 +- .../Http/Models/AnalysisRequestTests.cs | 66 ++++++++++++++ .../Http/Models/AnalysisResponseTests.cs | 89 +++++++++++++++++++ .../RoslynAnalysisServiceTests.cs | 4 +- .../Analysis/DiagnosticDuplicatesComparer.cs | 4 +- .../DiagnosticToRoslynIssueConverter.cs | 3 +- .../Analysis/RoslynIssue.cs | 6 +- .../SequentialRoslynAnalysisEngine.cs | 2 +- .../Http/AnalysisRequestHandler.cs | 2 +- .../Http/Models/AnalysisRequest.cs | 10 +-- .../RoslynAnalysisService.cs | 2 +- 16 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs create mode 100644 src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index 1fd9ea8918..cf46caf535 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,7 +9,7 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 - 1.0.0.81 + 1.0.0.89 10.34.0.83431 1.0.0 diff --git a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs index e3b71eaa68..f51c3e6049 100644 --- a/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs +++ b/src/RoslynAnalyzerServer.IntegrationTests/Http/RoslynAnalysisHttpServerTest.cs @@ -285,7 +285,7 @@ private static AnalysisRequestConfig CreateClientRequestConfig( token ??= ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Token; requestUri ??= GetRequestUrl(ServerStarter.HttpServerConfigurationProvider.CurrentConfiguration.Port); var fileUris = fileNames.Select(x => new FileUri(x)); - var analysisRequest = new AnalysisRequest { FileNames = [.. fileUris], ActiveRules = [new ActiveRuleDto("id", [])], AnalysisId = analysisId ?? Guid.NewGuid() }; + var analysisRequest = new AnalysisRequest { FileUris = [.. fileUris], ActiveRules = [new ActiveRuleDto("id", [])], AnalysisId = analysisId ?? Guid.NewGuid() }; return new AnalysisRequestConfig(token, requestUri, analysisRequest); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs index 8041eca5c4..6f83a8ec41 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticDuplicatesComparerTests.cs @@ -19,6 +19,7 @@ */ using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; @@ -26,7 +27,7 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; public class DiagnosticDuplicatesComparerTests { private readonly DiagnosticDuplicatesComparer testSubject = DiagnosticDuplicatesComparer.Instance; - private readonly RoslynIssue diagnostic1 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10); + private readonly RoslynIssue diagnostic1 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10); [TestMethod] public void Equals_SameReference_ReturnsTrue() @@ -55,7 +56,7 @@ public void Equals_SecondArgumentNull_ReturnsFalse() [TestMethod] public void Equals_SameRuleKeyAndLocation_ReturnsTrue() { - var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10); + var diagnostic2 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10); var result = testSubject.Equals(diagnostic1, diagnostic2); @@ -66,7 +67,7 @@ public void Equals_SameRuleKeyAndLocation_ReturnsTrue() [TestMethod] public void Equals_SameRuleKeyAndLocation_MessageIsIgnored() { - var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10, "some different message"); + var diagnostic2 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10, "some different message"); var result = testSubject.Equals(diagnostic1, diagnostic2); @@ -74,15 +75,15 @@ public void Equals_SameRuleKeyAndLocation_MessageIsIgnored() } [TestMethod] - [DataRow("rule2", "file1.cs", 1, 1, 1, 10, DisplayName = "Different RuleKey")] - [DataRow("rule1", "file2.cs", 1, 1, 1, 10, DisplayName = "Different FilePath")] - [DataRow("rule1", "file1.cs", 2, 1, 1, 10, DisplayName = "Different StartLine")] - [DataRow("rule1", "file1.cs", 1, 1, 2, 10, DisplayName = "Different EndLine")] - [DataRow("rule1", "file1.cs", 1, 2, 1, 10, DisplayName = "Different StartLineOffset")] - [DataRow("rule1", "file1.cs", 1, 1, 1, 11, DisplayName = "Different EndLineOffset")] + [DataRow("rule2", "file:///a/file1.cs", 1, 1, 1, 10, DisplayName = "Different RuleKey")] + [DataRow("rule1", "file:///a/file2.cs", 1, 1, 1, 10, DisplayName = "Different FilePath")] + [DataRow("rule1", "file:///a/file1.cs", 2, 1, 1, 10, DisplayName = "Different StartLine")] + [DataRow("rule1", "file:///a/file1.cs", 1, 1, 2, 10, DisplayName = "Different EndLine")] + [DataRow("rule1", "file:///a/file1.cs", 1, 2, 1, 10, DisplayName = "Different StartLineOffset")] + [DataRow("rule1", "file:///a/file1.cs", 1, 1, 1, 11, DisplayName = "Different EndLineOffset")] public void Equals_DifferentValues_ReturnsFalse(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset) { - var diagnostic2 = CreateDiagnostic(ruleKey, filePath, startLine, startLineOffset, endLine, endLineOffset); + var diagnostic2 = CreateDiagnostic(ruleKey, new FileUri(filePath), startLine, startLineOffset, endLine, endLineOffset); var result = testSubject.Equals(diagnostic1, diagnostic2); @@ -92,7 +93,7 @@ public void Equals_DifferentValues_ReturnsFalse(string ruleKey, string filePath, [TestMethod] public void GetHashCode_SameObjects_ReturnsSameHashCode() { - var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10); + var diagnostic2 = CreateDiagnostic("rule1", new FileUri("file:///a/file1.cs"), 1, 1, 1, 10); var hash1 = testSubject.GetHashCode(diagnostic1); var hash2 = testSubject.GetHashCode(diagnostic2); @@ -103,7 +104,7 @@ public void GetHashCode_SameObjects_ReturnsSameHashCode() [TestMethod] public void GetHashCode_DifferentObjects_ReturnsDifferentHashCodes() { - var diagnostic2 = CreateDiagnostic("rule2", "file2.cs", 2, 2, 2, 20); + var diagnostic2 = CreateDiagnostic("rule2", new FileUri("file:///a/file2.cs"), 2, 2, 2, 20); var hash1 = testSubject.GetHashCode(diagnostic1); var hash2 = testSubject.GetHashCode(diagnostic2); @@ -120,10 +121,10 @@ public void Instance_ReturnsSingletonInstance() instance1.Should().BeSameAs(instance2); } - private static RoslynIssue CreateDiagnostic(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset, string? message = null) + private static RoslynIssue CreateDiagnostic(string ruleKey, FileUri fileUri, int startLine, int startLineOffset, int endLine, int endLineOffset, string? message = null) { var textRange = new RoslynIssueTextRange(startLine, endLine, startLineOffset, endLineOffset); - var location = new RoslynIssueLocation(message ?? "message", filePath, textRange); + var location = new RoslynIssueLocation(message ?? "message", fileUri, textRange); return new RoslynIssue(ruleKey, location); } } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs index a618bdeae0..40f143f2e6 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/DiagnosticToRoslynIssueConverterTests.cs @@ -21,10 +21,11 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.TestInfrastructure; +using Language = SonarLint.VisualStudio.Core.Language; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis; @@ -57,13 +58,14 @@ public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( Language language, string ruleId, string message, - string filePath, + string fileName, int startLine, int endLine, int startChar, int endChar) { - var location = CreateLocation(filePath, startLine, endLine, startChar, endChar); + var fileUri = new FileUri(fileName); + var location = CreateLocation(fileUri, startLine, endLine, startChar, endChar); var diagnostic = CreateDiagnostic(ruleId, message, location); var expectedTextRange = new RoslynIssueTextRange( startLine + 1, // Convert to 1-based @@ -72,7 +74,7 @@ public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly( endChar); var expectedLocation = new RoslynIssueLocation( message, - filePath, + fileUri, expectedTextRange); var expectedRuleId = $"{language.RepoInfo.Key}:{ruleId}"; var expectedDiagnostic = new RoslynIssue( @@ -104,8 +106,8 @@ public void ConvertToSonarDiagnostic_WithSecondaryLocations_ConvertsCorrectly( { const string fileCs = "c:\\test\\file.cs"; const string file2Cs = "c:\\test\\file2.cs"; - var primaryLocation = CreateLocation(fileCs, 5, 5, 10, 15); - var additionalLocations = new[] { CreateLocation(fileCs, 10, 10, 20, 25), CreateLocation(file2Cs, 15, 15, 30, 35) }; + var primaryLocation = CreateLocation(new FileUri(fileCs), 5, 5, 10, 15); + var additionalLocations = new[] { CreateLocation(new FileUri(fileCs), 10, 10, 20, 25), CreateLocation(new FileUri(file2Cs), 15, 15, 30, 35) }; var diagnostic = CreateDiagnostic("any", "any", primaryLocation, additionalLocations, properties); var expectedFlows = new[] { @@ -113,11 +115,11 @@ public void ConvertToSonarDiagnostic_WithSecondaryLocations_ConvertsCorrectly( { new( expectedMessages[0], - fileCs, + new FileUri(fileCs), new RoslynIssueTextRange(11, 11, 20, 25)), new( expectedMessages[1], - file2Cs, + new FileUri(file2Cs), new RoslynIssueTextRange(16, 16, 30, 35)) }) }; @@ -130,7 +132,7 @@ public void ConvertToSonarDiagnostic_WithSecondaryLocations_ConvertsCorrectly( [TestMethod] public void ConvertToSonarDiagnostic_WithQuickFixes_ConvertsCorrectly() { - var diagnostic = CreateDiagnostic("any", "any", CreateLocation("any", 0, 0, 0, 0)); + var diagnostic = CreateDiagnostic("any", "any", CreateLocation(new FileUri("file:///C:/any.cs"), 0, 0, 0, 0)); var quickFix1 = new RoslynQuickFix(Guid.NewGuid()); var quickFix2 = new RoslynQuickFix(Guid.NewGuid()); @@ -143,7 +145,7 @@ public void ConvertToSonarDiagnostic_WithQuickFixes_ConvertsCorrectly() [TestMethod] public void ConvertToSonarDiagnostic_WithNoQuickFixes_ReturnsEmptyQuickFixesList() { - var diagnostic = CreateDiagnostic("any", "any", CreateLocation("any", 0, 0, 0, 0)); + var diagnostic = CreateDiagnostic("any", "any", CreateLocation(new FileUri("file:///C:/any.cs"), 0, 0, 0, 0)); var result = testSubject.ConvertToSonarDiagnostic(diagnostic, [], Language.CSharp); @@ -151,7 +153,7 @@ public void ConvertToSonarDiagnostic_WithNoQuickFixes_ReturnsEmptyQuickFixesList } private static Location CreateLocation( - string filePath, + FileUri fileUri, int startLine, int endLine, int startChar, @@ -162,7 +164,7 @@ private static Location CreateLocation( var linePositionSpan = new LinePositionSpan( new LinePosition(startLine, startChar), new LinePosition(endLine, endChar)); - syntaxTree.GetMappedLineSpan(textSpan, CancellationToken.None).Returns(new FileLinePositionSpan(filePath, linePositionSpan)); + syntaxTree.GetMappedLineSpan(textSpan, CancellationToken.None).Returns(new FileLinePositionSpan(fileUri.LocalPath, linePositionSpan)); return Location.Create(syntaxTree, textSpan); } diff --git a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs index b67129575d..63a06e9580 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Analysis/SequentialRoslynAnalysisEngineTests.cs @@ -26,6 +26,7 @@ using SonarLint.VisualStudio.Integration.TestInfrastructure; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Wrappers; +using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.TestInfrastructure; using Language = SonarLint.VisualStudio.Core.Language; @@ -125,7 +126,7 @@ public async Task AnalyzeAsync_DuplicateDiagnostics_ReturnsSingleDiagnostic() result.Should().BeEquivalentTo(duplicateIssue1); VerifyAnalysisExecution(requestForProject, compilationForProject, [duplicateDiagnostic1, duplicateDiagnostic2]); logger.AssertPartialOutputStringExists( - $"Duplicate diagnostic discarded ID: {duplicateIssue2.RuleId}, File: {duplicateIssue2.PrimaryLocation.FilePath}, Line: {duplicateIssue2.PrimaryLocation.TextRange.StartLine}"); + $"Duplicate diagnostic discarded ID: {duplicateIssue2.RuleId}, File: {duplicateIssue2.PrimaryLocation.FileUri.LocalPath}, Line: {duplicateIssue2.PrimaryLocation.TextRange.StartLine}"); } [TestMethod] @@ -142,7 +143,7 @@ public async Task AnalyzeAsync_DuplicateDiagnosticsInDifferentProjects_ReturnsSi VerifyAnalysisExecution(requestForProject1, compilationForProject1, [diagnostic1]); VerifyAnalysisExecution(requestForProject2, compilationForProject2, [diagnostic2]); logger.AssertPartialOutputStringExists( - $"Duplicate diagnostic discarded ID: {duplicateIssue.RuleId}, File: {duplicateIssue.PrimaryLocation.FilePath}, Line: {duplicateIssue.PrimaryLocation.TextRange.StartLine}"); + $"Duplicate diagnostic discarded ID: {duplicateIssue.RuleId}, File: {duplicateIssue.PrimaryLocation.FileUri.LocalPath}, Line: {duplicateIssue.PrimaryLocation.TextRange.StartLine}"); } [TestMethod] @@ -314,7 +315,7 @@ private static Diagnostic CreateTestDiagnostic(string id, string message) true); var location = Location.Create( - "test.cs", + "C:\\test.cs", new TextSpan(0, 1), new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1))); @@ -324,7 +325,7 @@ private static Diagnostic CreateTestDiagnostic(string id, string message) private static RoslynIssue CreateSonarIssue(string ruleId, string message) { var textRange = new RoslynIssueTextRange(1, 1, 0, 1); - var location = new RoslynIssueLocation(message, "test.cs", textRange); + var location = new RoslynIssueLocation(message, new FileUri("C:\\test.cs"), textRange); return new RoslynIssue(ruleId, location); } } diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs index b474c92033..9bc46d989d 100644 --- a/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs +++ b/src/RoslynAnalyzerServer.UnitTests/Http/AnalysisRequestHandlerTest.cs @@ -294,7 +294,7 @@ public async Task ParseAnalysisRequestBody_FileNamesEmpty_ReturnsNull() [TestMethod] public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel() { - var validRequestJson = $$"""{"FileNames":["{{FileUri}}"],"ActiveRules":[{"RuleId":"{{DiagnosticId}}"}], "AnalysisId":"{{AnalysisId}}"}"""; + var validRequestJson = $$"""{"FileUris":["{{FileUri}}"],"ActiveRules":[{"RuleId":"{{DiagnosticId}}"}], "AnalysisId":"{{AnalysisId}}"}"""; var stream = new MemoryStream(Encoding.UTF8.GetBytes(validRequestJson)); request.InputStream.Returns(stream); request.ContentEncoding.Returns(Encoding.UTF8); @@ -302,8 +302,8 @@ public async Task ParseAnalysisRequestBody_RequestBodyValid_ReturnsExpectedModel var result = await AnalysisRequestHandler.ParseAnalysisRequestBodyAsync(context.Request); result.Should().NotBeNull(); - result!.FileNames.Should().HaveCount(1); - result.FileNames[0].Should().Be(FileUri); + result!.FileUris.Should().HaveCount(1); + result.FileUris[0].Should().Be(FileUri); result.ActiveRules.Should().HaveCount(1); result.ActiveRules[0].RuleId.Should().Be(DiagnosticId); result.AnalysisId.Should().Be(AnalysisId); diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs new file mode 100644 index 0000000000..d8c51428ef --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisRequestTests.cs @@ -0,0 +1,66 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http.Models; + +[TestClass] +public class AnalysisRequestTests +{ + + [TestMethod] + public void Deserialization_SmokeTest() + { + var json = + """ + { + "FileUris": ["file:///C:/test/file1.cs", "file:///C:/test/file2.cs"], + "ActiveRules": [ + { "RuleId": "S101", "Parameters": { "threshold": "3" } }, + { "RuleId": "S102", "Parameters": { "timeout": "60" } } + ], + "AnalysisProperties": { "prop1": "value1", "prop2": "value2" }, + "AnalyzerInfo": { "ShouldUseCsharpEnterprise": true, "ShouldUseVbEnterprise": false }, + "AnalysisId": "8171cac6-65cc-4ba0-8804-db38f424f37d" + } + """; + + var expected = new AnalysisRequest + { + FileUris = + [ + new FileUri("file:///C:/test/file1.cs"), new FileUri("file:///C:/test/file2.cs") + ], + ActiveRules = + [ + new ActiveRuleDto("S101", new Dictionary { { "threshold", "3" } }), new ActiveRuleDto("S102", new Dictionary { { "timeout", "60" } }) + ], + AnalysisProperties = new Dictionary { { "prop1", "value1" }, { "prop2", "value2" } }, + AnalyzerInfo = new AnalyzerInfoDto(true, false), + AnalysisId = Guid.Parse("8171cac6-65cc-4ba0-8804-db38f424f37d") + }; + + var actual = JsonConvert.DeserializeObject(json); + actual.Should().BeEquivalentTo(expected, options => options.ComparingByMembers().ComparingByMembers().ComparingByMembers()); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs new file mode 100644 index 0000000000..95d26e50c2 --- /dev/null +++ b/src/RoslynAnalyzerServer.UnitTests/Http/Models/AnalysisResponseTests.cs @@ -0,0 +1,89 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using FluentAssertions; +using Newtonsoft.Json; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; +using SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; +using SonarLint.VisualStudio.SLCore.Common.Models; + +namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Http.Models; + +[TestClass] +public class AnalysisResponseTests +{ + [TestMethod] + public void Serialization_ProducesExpectedJson() + { + const string expectedJson = + """ + { + "RoslynIssues": [ + { + "RuleId": "rule-id", + "PrimaryLocation": { + "FileUri": "file:///c:/temp/test.cs", + "Message": "primary message", + "TextRange": { + "StartLine": 1, + "EndLine": 2, + "StartLineOffset": 3, + "EndLineOffset": 4 + } + }, + "Flows": [ + { + "Locations": [ + { + "FileUri": "file:///c:/temp/test.cs", + "Message": "secondary message", + "TextRange": { + "StartLine": 1, + "EndLine": 2, + "StartLineOffset": 3, + "EndLineOffset": 4 + } + } + ] + } + ], + "QuickFixes": [ + { + "Value": "fix value" + } + ] + } + ] + } + """; + var issueTextRange = new RoslynIssueTextRange(1, 2, 3, 4); + var fileUri = new FileUri("file:///c:/temp/test.cs"); + var primaryLocation = new RoslynIssueLocation("primary message", fileUri, issueTextRange); + var secondaryLocation = new RoslynIssueLocation("secondary message", fileUri, issueTextRange); + var flow = new RoslynIssueFlow([secondaryLocation]); + var quickFix = new RoslynIssueQuickFix("fix value"); + var issue = new RoslynIssue("rule-id", primaryLocation, [flow], [quickFix]); + var originalResponse = new AnalysisResponse { RoslynIssues = [issue] }; + + var actualJson = JsonConvert.SerializeObject(originalResponse, Formatting.Indented); + + actualJson.Should().Be(expectedJson); + } +} diff --git a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs index f5fc32f550..55b51c4200 100644 --- a/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs +++ b/src/RoslynAnalyzerServer.UnitTests/RoslynAnalysisServiceTests.cs @@ -38,7 +38,7 @@ public class RoslynAnalysisServiceTests private static readonly Dictionary DefaultAnalysisProperties = new() { { "sonar.cs.any", "any" } }; private static readonly Dictionary DefaultAnalysisConfigurations = new() { { Language.CSharp, new RoslynAnalysisConfiguration() } }; private static readonly List DefaultProjectAnalysisRequests = new() { new RoslynProjectAnalysisRequest(Substitute.For(), []) }; - private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new RoslynIssueLocation("any", "any", new RoslynIssueTextRange(1, 1, 1, 1))) }; + private static readonly List DefaultIssues = new() { new RoslynIssue("sample-rule-id", new RoslynIssueLocation("any", new FileUri("file:///C:/any.cs"), new RoslynIssueTextRange(1, 1, 1, 1))) }; private static readonly AnalyzerInfoDto DefaultAnalyzerInfoDto = new(false, false); private IRoslynSolutionAnalysisCommandProvider analysisCommandProvider = null!; private IRoslynAnalysisConfigurationProvider analysisConfigurationProvider = null!; @@ -185,7 +185,7 @@ private static AnalysisRequest CreateAnalysisRequest( return new AnalysisRequest { - FileNames = fileNames, + FileUris = fileNames, ActiveRules = DefaultActiveRules, AnalysisProperties = DefaultAnalysisProperties, AnalyzerInfo = DefaultAnalyzerInfoDto, diff --git a/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs index 9bd47c3178..79d4e710ab 100644 --- a/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticDuplicatesComparer.cs @@ -52,7 +52,7 @@ public int GetHashCode(RoslynIssue obj) { var hc = obj.RuleId.GetHashCode(); const int prime = 397; - hc = (hc * prime) ^ obj.PrimaryLocation.FilePath.GetHashCode(); + hc = (hc * prime) ^ obj.PrimaryLocation.FileUri.GetHashCode(); hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLine; hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.StartLineOffset; hc = (hc * prime) ^ obj.PrimaryLocation.TextRange.EndLine; @@ -62,7 +62,7 @@ public int GetHashCode(RoslynIssue obj) } private static bool LocationEquals(RoslynIssueLocation xPrimaryLocation, RoslynIssueLocation yPrimaryLocation) => - xPrimaryLocation.FilePath == yPrimaryLocation.FilePath && + xPrimaryLocation.FileUri == yPrimaryLocation.FileUri && xPrimaryLocation.TextRange.StartLine == yPrimaryLocation.TextRange.StartLine && xPrimaryLocation.TextRange.EndLine == yPrimaryLocation.TextRange.EndLine && xPrimaryLocation.TextRange.StartLineOffset == yPrimaryLocation.TextRange.StartLineOffset && diff --git a/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs b/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs index e69adcd789..c4f3909775 100644 --- a/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs +++ b/src/RoslynAnalyzerServer/Analysis/DiagnosticToRoslynIssueConverter.cs @@ -22,6 +22,7 @@ using Microsoft.CodeAnalysis; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.SLCore.Common.Models; namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; @@ -69,7 +70,7 @@ private static RoslynIssueLocation ConvertLocation(FileLinePositionSpan fileLine var location = new RoslynIssueLocation( message, - fileLinePositionSpan.Path, + new FileUri(fileLinePositionSpan.Path), textRange); return location; diff --git a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs index 4133ab647e..c43bb0f4f7 100644 --- a/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs +++ b/src/RoslynAnalyzerServer/Analysis/RoslynIssue.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using SonarLint.VisualStudio.SLCore.Common.Models; + namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis; public class RoslynIssue( @@ -44,9 +46,9 @@ public class RoslynIssueFlow(IReadOnlyList locations) public IReadOnlyList Locations { get; } = locations ?? throw new ArgumentNullException(nameof(locations)); } -public class RoslynIssueLocation(string message, string filePath, RoslynIssueTextRange textRange) +public class RoslynIssueLocation(string message, FileUri fileUri, RoslynIssueTextRange textRange) { - public string FilePath { get; } = filePath; + public FileUri FileUri { get; } = fileUri; public string Message { get; } = message; public RoslynIssueTextRange TextRange { get; } = textRange; } diff --git a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs index ea62f867bc..43a95de332 100644 --- a/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs +++ b/src/RoslynAnalyzerServer/Analysis/SequentialRoslynAnalysisEngine.cs @@ -62,7 +62,7 @@ public async Task> AnalyzeAsync( // todo SLVS-2468 improve issue merging if (!uniqueDiagnostics.Add(roslynIssue)) { - logger.LogVerbose(Resources.AnalysisEngine_DuplicateDiagnostic, roslynIssue.RuleId, Path.GetFileName(roslynIssue.PrimaryLocation.FilePath), roslynIssue.PrimaryLocation.TextRange.StartLine); + logger.LogVerbose(Resources.AnalysisEngine_DuplicateDiagnostic, roslynIssue.RuleId, roslynIssue.PrimaryLocation.FileUri.LocalPath, roslynIssue.PrimaryLocation.TextRange.StartLine); } } } diff --git a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs index 039f26862c..877f9f1b0e 100644 --- a/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs +++ b/src/RoslynAnalyzerServer/Http/AnalysisRequestHandler.cs @@ -78,7 +78,7 @@ public bool ValidateRequest(IHttpListenerRequest request, out HttpStatusCode err public async Task ParseAnalysisRequestBodyAsync(IHttpListenerRequest request) { var analysisRequestBodyAsync = await ParseAnalysisRequestBodyAsync(request); - return analysisRequestBodyAsync is { FileNames.Count: > 0, ActiveRules.Count: > 0 } ? analysisRequestBodyAsync : null; + return analysisRequestBodyAsync is { FileUris.Count: > 0, ActiveRules.Count: > 0 } ? analysisRequestBodyAsync : null; } public Task ParseCancellationRequestBodyAsync(IHttpListenerRequest request) => diff --git a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs index e1ce7cb867..44b71f4c94 100644 --- a/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs +++ b/src/RoslynAnalyzerServer/Http/Models/AnalysisRequest.cs @@ -26,11 +26,11 @@ namespace SonarLint.VisualStudio.RoslynAnalyzerServer.Http.Models; public record AnalysisRequest { [JsonRequired] - public List FileNames { get; set; } = []; + public List FileUris { get; init; } = []; [JsonRequired] - public List ActiveRules { get; set; } = []; - public Dictionary AnalysisProperties { get; set; } = []; - public AnalyzerInfoDto AnalyzerInfo { get; set; } = null!; + public List ActiveRules { get; init; } = []; + public Dictionary AnalysisProperties { get; init; } = []; + public AnalyzerInfoDto AnalyzerInfo { get; init; } = null!; [JsonRequired] - public Guid AnalysisId { get; set; } + public Guid AnalysisId { get; init; } } diff --git a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs index 0f9b9f852c..4954cdac64 100644 --- a/src/RoslynAnalyzerServer/RoslynAnalysisService.cs +++ b/src/RoslynAnalyzerServer/RoslynAnalysisService.cs @@ -45,7 +45,7 @@ public async Task> AnalyzeAsync( try { return await analysisEngine.AnalyzeAsync( - analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileNames.Select(x => x.LocalPath).ToArray()), + analysisCommandProvider.GetAnalysisCommandsForCurrentSolution(analysisRequest.FileUris.Select(x => x.LocalPath).ToArray()), await analysisConfigurationProvider.GetConfigurationAsync(analysisRequest.ActiveRules, analysisRequest.AnalysisProperties, analysisRequest.AnalyzerInfo), SetUpCancellationTokenForAnalysis(analysisRequest, cancellationToken)); } From b1d7b978c41f3537bd708d39c0ba5f09c1bfbafc Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:38:17 +0200 Subject: [PATCH 36/38] SLVS-2418, SLVS-2419, SLVS-2420, SLVS-2422 Remove old roslyn integration (#6463) [SLVS-2418](https://sonarsource.atlassian.net/browse/SLVS-2418) [SLVS-2419](https://sonarsource.atlassian.net/browse/SLVS-2419) [SLVS-2420](https://sonarsource.atlassian.net/browse/SLVS-2420) [SLVS-2422](https://sonarsource.atlassian.net/browse/SLVS-2422) Part of [SLVS-2418]: https://sonarsource.atlassian.net/browse/SLVS-2418?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SLVS-2419]: https://sonarsource.atlassian.net/browse/SLVS-2419?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SLVS-2420]: https://sonarsource.atlassian.net/browse/SLVS-2420?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SLVS-2422]: https://sonarsource.atlassian.net/browse/SLVS-2422?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- SonarQube.VisualStudio.sln | 24 - src/CFamily.UnitTests/packages.lock.json | 13 - .../Binding/SonarQubeRoslynRuleStatusTests.cs | 195 --- .../IssueMatcherTests.cs | 193 --- .../BindingJsonModelConverterTests.cs | 13 +- .../BindingJsonModelSerializationTests.cs | 31 +- .../SolutionBindingFileLoaderTests.cs | 29 +- .../ProjectRootCalculatorTests.cs | 92 -- .../QualityProfileUpdaterTests.cs | 218 --- .../ServerQueryInfoProviderTests.cs | 135 -- .../QualityProfileServerEventChannelTests.cs | 34 - ...QualityProfileServerEventsListenerTests.cs | 88 -- .../SSESessionFactoryTests.cs | 87 -- .../SSESessionManagerTests.cs | 340 ---- .../ServerSentEvents/SSESessionTests.cs | 288 ---- .../Binding/BoundSonarQubeProject.cs | 1 - .../BoundSonarQubeProjectExtensions.cs | 2 +- .../Binding/SonarQubeRuleStatus.cs | 62 - src/ConnectedMode/ConnectedMode.csproj | 10 - src/ConnectedMode/ConnectedModePackage.cs | 29 - src/ConnectedMode/IssueMatcher.cs | 123 -- .../Persistence/BindingJsonModel.cs | 1 - .../Persistence/BindingJsonModelConverter.cs | 5 +- .../Persistence/SolutionBindingRepository.cs | 16 +- src/ConnectedMode/ProjectRootCalculator.cs | 69 - .../QualityProfiles/QualityProfileUpdater.cs | 79 - .../QualityProfilesStrings.Designer.cs | 135 -- .../QualityProfilesStrings.resx | 148 -- .../RoslynQualityProfileDownloader.cs | 54 - src/ConnectedMode/ServerQueryInfoProvider.cs | 70 - .../Issue/IIssueServerEventSource.cs | 28 - .../Issue/IIssueServerEventSourcePublisher.cs | 30 - .../Issue/IServerSentEventSource.cs | 49 - .../Issue/IServerSentEventSourcePublisher.cs | 41 - .../Issue/IssueServerEventChannel.cs | 31 - .../IQualityProfileServerEventSource.cs | 30 - ...ualityProfileServerEventSourcePublisher.cs | 30 - .../QualityProfileServerEventChannel.cs | 31 - .../QualityProfileServerEventsListener.cs | 64 - .../ServerSentEvents/SSESessionFactory.cs | 212 --- .../ServerSentEvents/SSESessionManager.cs | 134 -- .../ServerSentEvents/ServerEventChannel.cs | 83 - .../Binding/BoundServerProjectTests.cs | 1 - .../ServerEventChannelTests.cs | 159 -- src/Core/Binding/BoundServerProject.cs | 1 - .../Roslyn/AnalyzerArrayComparerTests.cs | 125 -- .../AnalyzerAssemblyLoaderFactoryTests.cs | 56 - .../Roslyn/AnalyzerChangeTests.cs | 192 --- .../EmbeddedDotnetAnalyzerProviderTests.cs | 238 --- .../Roslyn/RoslynWorkspaceWrapperTests.cs | 174 --- .../Roslyn/AnalyzerArrayComparer.cs | 50 - .../Roslyn/AnalyzerAssemblyLoaderFactory.cs | 57 - .../Roslyn/EmbeddedDotnetAnalyzerProvider.cs | 78 - .../Roslyn/IAnalyzerAssemblyLoaderFactory.cs | 28 - .../Roslyn/IBasicRoslynAnalyzerProvider.cs | 33 - .../IEnterpriseRoslynAnalyzerProvider.cs | 34 - .../Roslyn/IObsoleteDotnetAnalyzersLocator.cs | 27 - .../Roslyn/IRoslynSolutionWrapper.cs | 92 -- .../Roslyn/IRoslynWorkspaceWrapper.cs | 103 -- .../ImportsBeforeFileGeneratorTests.cs | 198 --- .../CSharpVB/SonarLintConfigGeneratorTests.cs | 274 ---- .../Analysis/MuteIssueCommandTests.cs | 11 +- .../EmbeddedDotnetAnalyzersLocatorTests.cs | 80 - .../packages.lock.json | 13 - .../EmbeddedDotnetAnalyzersLocator.cs | 10 +- src/Integration.Vsix/Integration.Vsix.csproj | 6 - .../VS2022/source.extension.vsixmanifest | 12 - .../SonarLintDaemonPackage.cs | 4 - .../Install/ImportBeforeFileGenerator.cs | 102 -- .../CSharpVB/Install/SonarLintTargets.xml | 37 - src/Integration/Integration.csproj | 1 - .../InProcess/IssueConverterTests.cs | 88 -- .../NewSuppressedIssuesCalculatorTests.cs | 104 -- .../SuppressedIssuesCalculatorFactoryTests.cs | 87 -- .../SuppressedIssuesCalculatorTests.cs | 74 - .../SuppressedIssuesCalculatorTestsBase.cs | 54 - .../SuppressedIssuesRemovedCalculatorTests.cs | 102 -- .../Roslyn.Suppressions.UnitTests.csproj | 33 - .../RoslynSettingsFileInfoTests.cs | 53 - .../RoslynSettingsFileStorageTests.cs | 271 ---- .../RoslynSettingsFileWatcherTests.cs | 204 --- .../Settings.Cache/SettingsCacheTest.cs | 165 -- .../SonarDiagnosticSuppressorTests.cs | 244 --- .../SupportedSuppressionBuilderTests.cs | 71 - .../SuppressedIssueTests.cs | 96 -- .../SuppressionCheckerTests.cs | 401 ----- .../SuppressionExecutionContextTests.cs | 112 -- .../TestHelper.cs | 72 - .../packages.lock.json | 1377 ----------------- .../Roslyn.Suppressions/Container.cs | 83 - .../InProcess/ISuppressedIssuesCalculator.cs | 41 - .../InProcess/IssueConverter.cs | 76 - .../InProcess/SuppressedIssuesCalculator.cs | 85 - .../SuppressedIssuesCalculatorFactory.cs | 43 - .../Roslyn.Suppressions/InternalsVisibleTo.cs | 32 - .../EnableAllLoggerSettingsProvider.cs | 31 - .../Logging/SystemDebugLoggerWriter.cs | 30 - .../Resources/Strings.Designer.cs | 135 -- .../Resources/Strings.resx | 144 -- .../Roslyn.Suppressions.csproj | 61 - .../Roslyn.Suppressions/RoslynSettings.cs | 112 -- .../Settings.Cache/ISettingsCache.cs | 33 - .../Settings.Cache/SettingsCache.cs | 60 - .../SettingsFile/RoslynSettingsFileInfo.cs | 55 - .../SettingsFile/RoslynSettingsFileStorage.cs | 136 -- .../SettingsFile/RoslynSettingsFileWatcher.cs | 109 -- .../SonarDiagnosticSuppressor.cs | 88 -- .../SupportedSuppressionBuilder.g.cs | 563 ------- .../SupportedSuppressionBuilder.tt | 194 --- .../Roslyn.Suppressions/SuppressionChecker.cs | 142 -- .../SuppressionExecutionContext.cs | 63 - .../SuppressorCodeGen.targets | 94 -- .../Roslyn.Suppressions/packages.lock.json | 1292 ---------------- .../packages.lock.json | 13 - .../Helpers/ComponentKeyGeneratorTests.cs | 63 - .../SonarQubeIssueSeverityConverterTests.cs | 52 - .../SonarQubeIssueTypeConverterTests.cs | 51 - .../Models/IssueFlowTests.cs | 53 - .../Models/ServerExclusionsTests.cs | 233 --- .../BranchAndIssueKeyTests.cs | 47 - .../IssueChangedServerEventTests.cs | 67 - .../IssueChangedServerEventTests.cs | 47 - .../SSEStreamReaderFactoryTests.cs | 45 - .../ServerSentEvents/SSEStreamReaderTests.cs | 186 --- .../SqSSEStreamReaderTests.cs | 171 -- .../SqServerSentEventParserTests.cs | 191 --- .../Models/SonarQubeIssueTests.cs | 89 -- .../Api/V7_20/GetExclusionsRequestTests.cs | 144 -- .../Api/V7_20/GetIssuesRequestTests.cs | 466 ------ .../Api/V7_20/GetIssuesRequestWrapperTests.cs | 152 -- ...suesWithComponentSonarCloudRequestTests.cs | 92 -- ...ssuesWithComponentSonarQubeRequestTests.cs | 92 -- .../Api/V9_4/GetSonarLintEventStreamTests.cs | 58 - .../DefaultConfiguration_Configure_Tests.cs | 41 - .../SonarQube.Client.Tests.csproj | 10 + ...beService_CreateServerSentEventsSession.cs | 84 - .../SonarQubeService_GetIssuesBase.cs | 172 -- .../SonarQubeService_Miscellaneous.cs | 7 +- .../SonarQubeService_PagedRequestTests.cs | 267 ---- .../SonarQubeService_SearchFilesByName.cs | 176 --- .../SonarQubeService_TestBase.cs | 5 +- .../Api/Common/ApiExtensions.cs | 61 - .../Api/Common/ServerComponent.cs | 41 - .../Api/Common/ServerImpact.cs | 33 - .../Api/Common/ServerIssueTextRange.cs | 39 - .../Api/DefaultConfiguration.cs | 20 +- .../Api/IGetExclusionsRequest.cs | 30 - src/SonarQube.Client/Api/IGetIssuesRequest.cs | 47 - .../Api/IGetPropertiesRequest.cs | 30 - .../Api/IGetQualityProfilesRequest.cs | 32 - src/SonarQube.Client/Api/IGetRulesRequest.cs | 34 - .../Api/IGetSonarLintEventStream.cs | 31 - .../Api/ISearchFilesByNameRequest.cs | 31 - .../Api/V10_2/GetIssuesWithCCTRequest.cs | 77 - .../Api/V10_2/GetRulesWithCCTRequest.cs | 59 - .../Api/V2_60/GetPropertiesRequest.cs | 53 - .../Api/V5_20/GetQualityProfilesRequest.cs | 94 -- .../Api/V5_50/GetRulesRequest.cs | 154 -- .../Api/V6_30/GetPropertiesRequest.cs | 103 -- .../Api/V6_50/GetQualityProfilesRequest.cs | 30 - .../Api/V7_20/GetExclusionsRequest.cs | 85 - .../Api/V7_20/GetIssuesRequest.cs | 186 --- .../Api/V7_20/GetIssuesRequestWrapper.cs | 108 -- .../V7_20/GetIssuesWithComponentRequest.cs | 54 - .../Api/V9_4/GetSonarLintEventStream.cs | 59 - .../Api/V9_6/GetIssuesWithContextRequest.cs | 34 - .../Api/V9_9/SearchFilesByNameRequest.cs | 54 - .../Helpers/CleanCodeTaxonomyHelpers.cs | 48 - .../Helpers/ComponentKeyGenerator.cs | 52 - src/SonarQube.Client/ISonarQubeService.cs | 22 - .../Models/ServerExclusions.cs | 108 -- .../IIssueChangedServerEvent.cs | 95 -- .../ClientContract/IQualityProfileEvent.cs | 35 - .../ClientContract/IServerEvent.cs | 26 - .../ServerSentEvents/SSEStreamReader.cs | 103 -- .../SSEStreamReaderFactory.cs | 50 - .../ServerContract/ISqServerEvent.cs | 47 - .../ISqServerSentEventParser.cs | 80 - .../ServerContract/SqSSEStreamReader.cs | 98 -- .../Models/SonarQubeCleanCodeTaxonomy.cs | 63 - src/SonarQube.Client/Models/SonarQubeIssue.cs | 135 -- .../Models/SonarQubeIssueSeverity.cs | 47 - .../Models/SonarQubeIssueType.cs | 51 - .../Models/SonarQubeProject.cs | 39 - .../Models/SonarQubeProperty.cs | 37 - .../Models/SonarQubeQualityProfile.cs | 45 - src/SonarQube.Client/Models/SonarQubeRule.cs | 81 - .../Requests/IPagedRequest.cs | 34 - .../Requests/PagedRequestBase.cs | 91 -- src/SonarQube.Client/SonarQubeService.cs | 39 +- .../Helpers/DummySonarQubeIssueFactory.cs | 31 - 191 files changed, 37 insertions(+), 18926 deletions(-) delete mode 100644 src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs delete mode 100644 src/ConnectedMode.UnitTests/IssueMatcherTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs delete mode 100644 src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs delete mode 100644 src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs delete mode 100644 src/ConnectedMode/Binding/SonarQubeRuleStatus.cs delete mode 100644 src/ConnectedMode/IssueMatcher.cs delete mode 100644 src/ConnectedMode/ProjectRootCalculator.cs delete mode 100644 src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs delete mode 100644 src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs delete mode 100644 src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx delete mode 100644 src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs delete mode 100644 src/ConnectedMode/ServerQueryInfoProvider.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/SSESessionManager.cs delete mode 100644 src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs delete mode 100644 src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs delete mode 100644 src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs delete mode 100644 src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs delete mode 100644 src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs delete mode 100644 src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs delete mode 100644 src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs delete mode 100644 src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs delete mode 100644 src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs delete mode 100644 src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs delete mode 100644 src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs delete mode 100644 src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs delete mode 100644 src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs delete mode 100644 src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs delete mode 100644 src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs delete mode 100644 src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs delete mode 100644 src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs delete mode 100644 src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs delete mode 100644 src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs delete mode 100644 src/Integration/CSharpVB/Install/SonarLintTargets.xml delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets delete mode 100644 src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json delete mode 100644 src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs delete mode 100644 src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs delete mode 100644 src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/IssueFlowTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs delete mode 100644 src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs delete mode 100644 src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs delete mode 100644 src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs delete mode 100644 src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs delete mode 100644 src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs delete mode 100644 src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs delete mode 100644 src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs delete mode 100644 src/SonarQube.Client/Api/Common/ApiExtensions.cs delete mode 100644 src/SonarQube.Client/Api/Common/ServerComponent.cs delete mode 100644 src/SonarQube.Client/Api/Common/ServerImpact.cs delete mode 100644 src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs delete mode 100644 src/SonarQube.Client/Api/IGetExclusionsRequest.cs delete mode 100644 src/SonarQube.Client/Api/IGetIssuesRequest.cs delete mode 100644 src/SonarQube.Client/Api/IGetPropertiesRequest.cs delete mode 100644 src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs delete mode 100644 src/SonarQube.Client/Api/IGetRulesRequest.cs delete mode 100644 src/SonarQube.Client/Api/IGetSonarLintEventStream.cs delete mode 100644 src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs delete mode 100644 src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs delete mode 100644 src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs delete mode 100644 src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs delete mode 100644 src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs delete mode 100644 src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs delete mode 100644 src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs delete mode 100644 src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs delete mode 100644 src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs delete mode 100644 src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs delete mode 100644 src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs delete mode 100644 src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs delete mode 100644 src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs delete mode 100644 src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs delete mode 100644 src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs delete mode 100644 src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs delete mode 100644 src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs delete mode 100644 src/SonarQube.Client/Models/ServerExclusions.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs delete mode 100644 src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeIssue.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeIssueType.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeProject.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeProperty.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeQualityProfile.cs delete mode 100644 src/SonarQube.Client/Models/SonarQubeRule.cs delete mode 100644 src/SonarQube.Client/Requests/IPagedRequest.cs delete mode 100644 src/SonarQube.Client/Requests/PagedRequestBase.cs delete mode 100644 src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs diff --git a/SonarQube.VisualStudio.sln b/SonarQube.VisualStudio.sln index 8e7584bf23..1eeb518fb7 100644 --- a/SonarQube.VisualStudio.sln +++ b/SonarQube.VisualStudio.sln @@ -82,12 +82,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CFamily", "src\CFamily\CFam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CFamily.UnitTests", "src\CFamily.UnitTests\CFamily.UnitTests.csproj", "{30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Roslyn.Suppressions", "Roslyn.Suppressions", "{16BF2D77-AE3B-4218-A3E8-875829D73B00}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.Suppressions", "src\Roslyn.Suppressions\Roslyn.Suppressions\Roslyn.Suppressions.csproj", "{082D5D8E-F914-4139-9AE3-3F48B679E3DA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.Suppressions.UnitTests", "src\Roslyn.Suppressions\Roslyn.Suppressions.UnitTests\Roslyn.Suppressions.UnitTests.csproj", "{C478DAE7-58BC-4D02-929E-E413B40F2517}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConnectedMode", "ConnectedMode", "{3B4A8B40-9821-4964-8EAB-1D8A0B078292}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectedMode", "src\ConnectedMode\ConnectedMode.csproj", "{0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}" @@ -271,22 +265,6 @@ Global {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}.Release|Any CPU.Build.0 = Release|Any CPU {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}.Release|x86.ActiveCfg = Release|Any CPU {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3}.Release|x86.Build.0 = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|x86.ActiveCfg = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Debug|x86.Build.0 = Debug|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|Any CPU.Build.0 = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|x86.ActiveCfg = Release|Any CPU - {082D5D8E-F914-4139-9AE3-3F48B679E3DA}.Release|x86.Build.0 = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|x86.ActiveCfg = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Debug|x86.Build.0 = Debug|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|Any CPU.Build.0 = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|x86.ActiveCfg = Release|Any CPU - {C478DAE7-58BC-4D02-929E-E413B40F2517}.Release|x86.Build.0 = Release|Any CPU {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -437,8 +415,6 @@ Global {407919AF-3AEA-483D-9183-1063EDECDBC3} = {BE58232B-BE46-4D0D-9175-6BE0517F3EF0} {58619C0F-0F3D-4E8C-B204-A19B332D45E5} = {11D4BFC7-C1F2-45AC-888E-25A6A216AD1D} {30E1FF8F-94BA-4A39-A737-8FFD7B4A0CD3} = {11D4BFC7-C1F2-45AC-888E-25A6A216AD1D} - {082D5D8E-F914-4139-9AE3-3F48B679E3DA} = {16BF2D77-AE3B-4218-A3E8-875829D73B00} - {C478DAE7-58BC-4D02-929E-E413B40F2517} = {16BF2D77-AE3B-4218-A3E8-875829D73B00} {0BE551DB-3C46-42A5-BB38-DA80E83F8ABD} = {3B4A8B40-9821-4964-8EAB-1D8A0B078292} {2BD38A3A-6F0E-452B-A5B2-200113A32184} = {3B4A8B40-9821-4964-8EAB-1D8A0B078292} {67BEB251-4EA5-44EE-92A7-B4F57D9A6867} = {25DE7210-DFC0-448B-894E-84C1C9CA223E} diff --git a/src/CFamily.UnitTests/packages.lock.json b/src/CFamily.UnitTests/packages.lock.json index 5860db2956..cd6914598b 100644 --- a/src/CFamily.UnitTests/packages.lock.json +++ b/src/CFamily.UnitTests/packages.lock.json @@ -1260,7 +1260,6 @@ "SonarLint.VisualStudio.Integration": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", - "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", @@ -1377,18 +1376,6 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" - } - }, "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { diff --git a/src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs b/src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs deleted file mode 100644 index 81fce6817c..0000000000 --- a/src/ConnectedMode.UnitTests/Binding/SonarQubeRoslynRuleStatusTests.cs +++ /dev/null @@ -1,195 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarQube.Client.Models; -using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.Binding; - -[TestClass] -public class SonarQubeRoslynRuleStatusTests -{ - private IEnvironmentSettings environmentSettings; - - [TestInitialize] - public void TestInitialize() - { - environmentSettings = Substitute.For(); - } - - [TestMethod] - [DataRow(SonarQubeIssueSeverity.Info, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Minor, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Major, RuleAction.Warning)] - [DataRow(SonarQubeIssueSeverity.Critical, RuleAction.Warning)] - public void GetVSSeverity_NotBlocker_CorrectlyMapped(SonarQubeIssueSeverity sqSeverity, RuleAction expectedVsSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(sqSeverity), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - [DataRow(true, RuleAction.Error)] - [DataRow(false, RuleAction.Warning)] - public void GetVSSeverity_Blocker_CorrectlyMapped(bool shouldTreatBlockerAsError, RuleAction expectedVsSeverity) - { - environmentSettings.TreatBlockerSeverityAsError().Returns(shouldTreatBlockerAsError); - - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(SonarQubeIssueSeverity.Blocker), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - [DataRow(SonarQubeIssueSeverity.Unknown)] - [DataRow((SonarQubeIssueSeverity)(-1))] - public void GetVSSeverity_Invalid_Throws(SonarQubeIssueSeverity sqSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(sqSeverity), environmentSettings); - - Action act = () => testSubject.GetSeverity(); - - act.Should().Throw(); - } - - [DataTestMethod] - [DataRow(SonarQubeSoftwareQualitySeverity.Info, RuleAction.Info)] - [DataRow(SonarQubeSoftwareQualitySeverity.Low, RuleAction.Info)] - [DataRow(SonarQubeSoftwareQualitySeverity.Medium, RuleAction.Warning)] - public void GetVSSeverity_FromSoftwareQualitySeverity_NotBlocker_CorrectlyMapped(SonarQubeSoftwareQualitySeverity sqSeverity, RuleAction expectedVsSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus( - CreateMqrRule(sqSeverity), - environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - [DataRow(SonarQubeSoftwareQualitySeverity.High, true, RuleAction.Error)] - [DataRow(SonarQubeSoftwareQualitySeverity.High, false, RuleAction.Warning)] - [DataRow(SonarQubeSoftwareQualitySeverity.Blocker, true, RuleAction.Error)] - [DataRow(SonarQubeSoftwareQualitySeverity.Blocker, false, RuleAction.Warning)] - public void GetVSSeverity_FromSoftwareQualitySeverity_Blocker_CorrectlyMapped(SonarQubeSoftwareQualitySeverity sqSeverity, bool shouldTreatBlockerAsError, RuleAction expectedVsSeverity) - { - environmentSettings.TreatBlockerSeverityAsError().Returns(shouldTreatBlockerAsError); - - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule(sqSeverity), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [TestMethod] - public void GetVSSeverity_FromSoftwareQualitySeverity_Invalid_Throws() - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule((SonarQubeSoftwareQualitySeverity)(-1)), environmentSettings); - - Action act = () => testSubject.GetSeverity(); - - act.Should().Throw(); - } - - [DynamicData(nameof(MultipleMqrSeveritiesAndHighestConvertedVsSeverity))] - [DataTestMethod] - public void GetVSSeverity_FromSoftwareQualitySeverity_Multiple_TakesHighest(SonarQubeSoftwareQualitySeverity[] severities, RuleAction expectedAction) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule(severities), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedAction); - } - - [DataRow(SonarQubeIssueSeverity.Info, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Minor, RuleAction.Info)] - [DataRow(SonarQubeIssueSeverity.Major, RuleAction.Warning)] - [DataRow(SonarQubeIssueSeverity.Critical, RuleAction.Warning)] - [DataTestMethod] - public void GetVSSeverity_EmptyMqrSeverities_UsesStandardSeverity(SonarQubeIssueSeverity sqSeverity, RuleAction expectedVsSeverity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateRule(new(), sqSeverity), environmentSettings); - - testSubject.GetSeverity().Should().Be(expectedVsSeverity); - } - - [DynamicData(nameof(AllMqrSeverities))] - [DataTestMethod] - public void GetVSSeverity_HasSoftwareQualitySeverity_Inactive_ReturnsNone(SonarQubeSoftwareQualitySeverity severities) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateMqrRule(isActive: false, severities), environmentSettings); - - testSubject.GetSeverity().Should().Be(RuleAction.None); - } - - [DynamicData(nameof(AllSeverities))] - [DataTestMethod] - public void GetVSSeverity_HasSeverity_Inactive_ReturnsNone(SonarQubeIssueSeverity severity) - { - var testSubject = new SonarQubeRoslynRuleStatus(CreateStandardRule(severity, isActive: false), environmentSettings); - - testSubject.GetSeverity().Should().Be(RuleAction.None); - } - - public static object[][] MultipleMqrSeveritiesAndHighestConvertedVsSeverity => - [ - [new[] { SonarQubeSoftwareQualitySeverity.Blocker, SonarQubeSoftwareQualitySeverity.Low }, RuleAction.Warning], - [new[] { SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Blocker }, RuleAction.Warning], - [new[] { SonarQubeSoftwareQualitySeverity.Blocker, SonarQubeSoftwareQualitySeverity.Blocker }, RuleAction.Warning], - [new[] { SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Info }, RuleAction.Info], - [new[] { SonarQubeSoftwareQualitySeverity.Low, SonarQubeSoftwareQualitySeverity.Info, SonarQubeSoftwareQualitySeverity.High }, RuleAction.Warning], - ]; - - public static object[][] AllMqrSeverities => - Enum.GetValues(typeof(SonarQubeSoftwareQualitySeverity)).Cast() - .Select(severity => new[] { severity }) - .ToArray(); - - public static object[][] AllSeverities => - Enum.GetValues(typeof(SonarQubeIssueSeverity)).Cast() - .Select(severity => new[] { severity }) - .ToArray(); - - private static SonarQubeRule CreateMqrRule(params SonarQubeSoftwareQualitySeverity[] mqrSeverities) => CreateMqrRule(isActive: true, mqrSeverities); - - private static SonarQubeRule CreateMqrRule(bool isActive, params SonarQubeSoftwareQualitySeverity[] mqrSeverities) - { - mqrSeverities.Should().NotBeEmpty(); - var sonarQubeSoftwareQualitySeverities = - Enum.GetValues(typeof(SonarQubeSoftwareQuality)) - .Cast() - .Zip(mqrSeverities, (x, y) => (x, y)) - .ToDictionary(k => k.x, v => v.y); - return CreateRule(sonarQubeSoftwareQualitySeverities, SonarQubeIssueSeverity.Blocker, isActive: isActive); - } - - private static SonarQubeRule CreateStandardRule(SonarQubeIssueSeverity severity, bool isActive = true) => CreateRule(null, severity, isActive); - - private static SonarQubeRule CreateRule(Dictionary mqrSeverity, SonarQubeIssueSeverity severity, bool isActive = true) => - new(default, - default, - isActive, - severity, - default, - mqrSeverity, - default, - default); -} diff --git a/src/ConnectedMode.UnitTests/IssueMatcherTests.cs b/src/ConnectedMode.UnitTests/IssueMatcherTests.cs deleted file mode 100644 index b3d77f46a8..0000000000 --- a/src/ConnectedMode.UnitTests/IssueMatcherTests.cs +++ /dev/null @@ -1,193 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; - -[TestClass] -public class IssueMatcherTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported(); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", 1, "CorrectHash", true)] // exact matches - [DataRow("correctRULEID", 1, "CorrectHash", true)] // rule-id is case-insensitive - [DataRow("CorrectRuleId", 1, "wrong hash", true)] // matches on line - [DataRow("CorrectRuleId", 9999, "CorrectHash", true)] // matches on hash only - [DataRow("CorrectRuleId", 2, "correcthash", false)] // hash is case-sensitive - [DataRow("CorrectRuleId", 2, "wrong hash", false)] // wrong line and hash - [DataRow("CorrectRuleId", null, null, false)] // server file issue - [DataRow("wrong rule Id", 1, "CorrectHash", false)] - public void IsMatch_MatchesBasedOnAllParameters(string serverRuleId, int? serverIssueLine, - string serverHash, bool expectedResult) - { - var issueToMatch = CreateIssueToMatch("CorrectRuleId", 1, "CorrectHash"); - var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", null, null, true)] // exact matches - [DataRow("CorrectRuleId", null, "hash", true)] // hash should be ignored for file-level issues - [DataRow("WrongRuleId", null, null, false)] // wrong rule - [DataRow("CorrectRuleId", 1, "hash", false)] // not a file issue - [DataRow("CorrectRuleId", 999, null, false)] // not a file issue - should not match a file issue, even though the hash is the same - public void IsMatch_FileLevelIssue(string serverRuleId, int? serverIssueLine, - string serverHash, bool expectedResult) - { - // File issues have line number of 0 and an empty hash - var issueToMatch = CreateIssueToMatch("CorrectRuleId", null, null); - var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", null, null, true)] // exact matches - [DataRow("CorrectRuleId", null, "hash", true)] // hash should be ignored for file-level issues - [DataRow("WrongRuleId", null, null, false)] // wrong rule - [DataRow("CorrectRuleId", 20, "hash", true)] // roslyn issue is not actually a file issue, so it should match - [DataRow("CorrectRuleId", 1, null, true)] // roslyn issue is not actually a file issue, so it should match - [DataRow("CorrectRuleId", 999, null, false)] // not a file issue - should not match a file issue, even though the hash is the same - public void IsMatch_PotentialRoslynFileLevelIssue_ServerIssueVariation(string serverRuleId, int? serverIssueLine, - string serverHash, bool expectedResult) - { - // File issues for roslyn have line and column number equal to 1 - var issueToMatch = new FilterableRoslynIssue("CorrectRuleId", null, 1, 1); - issueToMatch.SetLineHash("hash"); // hash is always calculated since we don't know if it's a file issue or not - var serverIssue = CreateServerIssue(serverRuleId, serverIssueLine, serverHash); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [DataTestMethod] - [DataRow("CorrectRuleId", 1, 1, true)] // potential file issue matches server file issue - [DataRow("CorrectRuleId", 1, 2, false)] // not a file issue - [DataRow("WrongRuleId", 1, 1, false)] // wrong rule - public void IsMatch_PotentialRoslynFileLevelIssue_RoslynIssueVariation(string roslynIssueId, int startLine, int startColumn, bool expectedResult) - { - var issueToMatch = new FilterableRoslynIssue(roslynIssueId, null, startLine, startColumn); - issueToMatch.SetLineHash("hash"); // hash is always calculated since we don't know if it's a file issue or not - var serverIssue = CreateServerIssue("CorrectRuleId", null, null); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expectedResult); - } - - [TestMethod] - // Module-level issues i.e. no file - [DataRow(null, null, true)] - [DataRow(null, "", true)] - [DataRow("", null, true)] - [DataRow("", "", true)] - - // Module-level issues should not match non-module-level issues - [DataRow(@"any.txt", "", false)] - [DataRow(@"any.txt", null, false)] - [DataRow("", @"c:\any.txt", false)] - [DataRow(null, @"c:\any.txt", false)] - - // File issues - [DataRow(@"same.txt", @"c:\same.txt", true)] - [DataRow(@"SAME.TXT", @"c:\same.txt", true)] - [DataRow(@"same.TXT", @"c:\XXXsame.txt", false)] // partial file name -> should not match - [DataRow(@"differentExt.123", @"a:\differentExt.999", false)] // different extension -> should not match - [DataRow(@"aaa\partial\file.cs", @"d:\partial\file.cs", false)] - // Only matching the local path tail, so the same server path can match multiple local files - [DataRow(@"partial\file.cs", @"c:\aaa\partial\file.cs", true)] - [DataRow(@"partial\file.cs", @"c:\aaa\bbb\partial\file.cs", true)] - [DataRow(@"partial\file.cs", @"c:\aaa\bbb\ccc\partial\file.cs", true)] - public void IsMatch_CheckFileComparisons(string serverFilePath, string localFilePath, bool expected) - { - var issueToMatch = CreateIssueToMatch("111", 0, "hash", filePath: localFilePath); - - var serverIssue = CreateServerIssue("111", 0, "hash", filePath: serverFilePath); - - CreateTestSubject().IsLikelyMatch(issueToMatch, serverIssue).Should().Be(expected); - } - - [TestMethod] - public void FindMatchOrDefault_FindsFirstMatch() - { - var ruleId = "111"; - var startLine = 0; - var issueToMatch = CreateIssueToMatch(ruleId, startLine, null, @"c:\root\dir\file.cs"); - var serverPath = @"dir\file.cs"; - - var correctServerIssue = CreateServerIssue(ruleId, startLine, null, serverPath); - - CreateTestSubject().GetFirstLikelyMatchFromSameFileOrNull(issueToMatch, new[] - { - CreateServerIssue("222", startLine, null, serverPath), - CreateServerIssue(ruleId, 111, null, serverPath), - correctServerIssue, - CreateServerIssue(ruleId, startLine, null, serverPath) // finds only the firs match - }).Should().BeSameAs(correctServerIssue); - } - - [TestMethod] - public void FindMatchOrDefault_NoServerIssues_ReturnsNull() - { - var issueToMatch = CreateIssueToMatch("1", 1, "1"); - - CreateTestSubject().GetFirstLikelyMatchFromSameFileOrNull(issueToMatch, Array.Empty()).Should().BeNull(); - } - - private IssueMatcher CreateTestSubject() - { - return new IssueMatcher(); - } - - private IFilterableIssue CreateIssueToMatch(string ruleId, int? startLine, string lineHash, string filePath = null) => - new TestFilterableIssue - { - RuleId = ruleId, - StartLine = startLine, - LineHash = lineHash, - FilePath = filePath - }; - - private SonarQubeIssue CreateServerIssue(string ruleId, int? startLine, string lineHash, - string filePath = null) - { - var sonarQubeIssue = new SonarQubeIssue(null, filePath, lineHash, null, null, ruleId, false, SonarQubeIssueSeverity.Info, - DateTimeOffset.MinValue, DateTimeOffset.MinValue, - startLine.HasValue - ? new IssueTextRange(startLine.Value, 1, 1, 1) - : null, - flows: null); - - return sonarQubeIssue; - } -} diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs index 0eab8a2dfa..e80c875296 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelConverterTests.cs @@ -57,7 +57,6 @@ public void ConvertFromModel_ConvertsCorrectly() var bindingModel = new BindingJsonModel { ProjectKey = "project123", - Profiles = new Dictionary(), ProjectName = "ignored", Organization = new SonarQubeOrganization("ignored", "ignored"), ServerUri = new Uri("http://ignored"), @@ -70,13 +69,12 @@ public void ConvertFromModel_ConvertsCorrectly() boundServerProject.ServerConnection.Should().BeSameAs(connection); boundServerProject.LocalBindingKey.Should().BeSameAs(localBindingKey); boundServerProject.ServerProjectKey.Should().BeSameAs(bindingModel.ProjectKey); - boundServerProject.Profiles.Should().BeSameAs(bindingModel.Profiles); } [TestMethod] public void ConvertToModel_SonarCloudConnection_ConvertsCorrectly() { - var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarCloud("myorg")) { Profiles = new Dictionary() }; + var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarCloud("myorg")); var bindingModel = testSubject.ConvertToModel(boundServerProject); @@ -85,16 +83,12 @@ public void ConvertToModel_SonarCloudConnection_ConvertsCorrectly() bindingModel.ServerUri.Should().BeEquivalentTo(boundServerProject.ServerConnection.ServerUri); bindingModel.Organization.Key.Should().BeSameAs(((ServerConnection.SonarCloud)boundServerProject.ServerConnection).OrganizationKey); bindingModel.ServerConnectionId.Should().BeSameAs(boundServerProject.ServerConnection.Id); - bindingModel.Profiles.Should().BeSameAs(boundServerProject.Profiles); } [TestMethod] public void ConvertToModel_SonarQubeConnection_ConvertsCorrectly() { - var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarQube(new Uri("http://mysq"))) - { - Profiles = new Dictionary() - }; + var boundServerProject = new BoundServerProject("localBinding", "serverProject", new ServerConnection.SonarQube(new Uri("http://mysq"))); var bindingModel = testSubject.ConvertToModel(boundServerProject); @@ -103,7 +97,6 @@ public void ConvertToModel_SonarQubeConnection_ConvertsCorrectly() bindingModel.ServerUri.Should().BeEquivalentTo(boundServerProject.ServerConnection.ServerUri); bindingModel.Organization.Should().BeNull(); bindingModel.ServerConnectionId.Should().BeSameAs(boundServerProject.ServerConnection.Id); - bindingModel.Profiles.Should().BeSameAs(boundServerProject.Profiles); } [TestMethod] @@ -116,7 +109,6 @@ public void ConvertFromModelToLegacy_ConvertsCorrectly() ServerUri = new Uri("http://localhost"), ProjectKey = "project123", ProjectName = "project 123", - Profiles = new Dictionary(), ServerConnectionId = "ignored", }; @@ -126,7 +118,6 @@ public void ConvertFromModelToLegacy_ConvertsCorrectly() legacyBinding.ProjectName.Should().BeSameAs(bindingModel.ProjectName); legacyBinding.ServerUri.Should().BeSameAs(bindingModel.ServerUri); legacyBinding.Organization.Should().BeSameAs(bindingModel.Organization); - legacyBinding.Profiles.Should().BeSameAs(legacyBinding.Profiles); legacyBinding.Credentials.Should().BeSameAs(credentials); } } diff --git a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs index f9d9c56ca4..ce4c281c90 100644 --- a/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/BindingJsonModelSerializationTests.cs @@ -36,7 +36,7 @@ public class BindingJsonModelSerializationTests "my_project_123", "My Project", /* ignored */ null, - new SonarQubeOrganization("org_key_123", "My Org")) { Profiles = QualityProfiles }; + new SonarQubeOrganization("org_key_123", "My Org")); private readonly BindingJsonModel bindingJsonModel = new() { @@ -45,12 +45,11 @@ public class BindingJsonModelSerializationTests ProjectName = "My Project", Organization = new SonarQubeOrganization("org_key_123", "My Org"), ServerConnectionId = "some_connection_id_123", - Profiles = QualityProfiles }; - private readonly BoundServerProject boundSonarCloudServerProject = new("solution123", "my_project_123", new ServerConnection.SonarCloud("org_key_123")) { Profiles = QualityProfiles }; + private readonly BoundServerProject boundSonarCloudServerProject = new("solution123", "my_project_123", new ServerConnection.SonarCloud("org_key_123")); private readonly BoundServerProject boundSonarQubeServerProject - = new("solution123", "my_project_123", new ServerConnection.SonarQube(new Uri("http://next.sonarqube.com/sonarqube"))) { Profiles = QualityProfiles }; + = new("solution123", "my_project_123", new ServerConnection.SonarQube(new Uri("http://next.sonarqube.com/sonarqube"))); private readonly BindingJsonModelConverter bindingJsonModelConverter = new(); @@ -69,13 +68,7 @@ public void JsonModel_SerializedAsExpected() "Name": "My Org" }, "ProjectKey": "my_project_123", - "ProjectName": "My Project", - "Profiles": { - "C": { - "ProfileKey": "qpkey", - "ProfileTimestamp": "2020-12-31T23:59:59" - } - } + "ProjectName": "My Project" } """); } @@ -94,13 +87,7 @@ public void JsonModel_FromSonarCloudBinding_SerializedAsExpected() "Key": "org_key_123", "Name": null }, - "ProjectKey": "my_project_123", - "Profiles": { - "C": { - "ProfileKey": "qpkey", - "ProfileTimestamp": "2020-12-31T23:59:59" - } - } + "ProjectKey": "my_project_123" } """); } @@ -115,13 +102,7 @@ public void JsonModel_FromSonarQubeBinding_SerializedAsExpected() { "ServerConnectionId": "http://next.sonarqube.com/sonarqube", "ServerUri": "http://next.sonarqube.com/sonarqube", - "ProjectKey": "my_project_123", - "Profiles": { - "C": { - "ProfileKey": "qpkey", - "ProfileTimestamp": "2020-12-31T23:59:59" - } - } + "ProjectKey": "my_project_123" } """); } diff --git a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs index 2ac8f97e8b..72a2afef54 100644 --- a/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs +++ b/src/ConnectedMode.UnitTests/Persistence/SolutionBindingFileLoaderTests.cs @@ -55,22 +55,12 @@ public void TestInitialize() ProjectKey = "MyProject Key", ProjectName = "projectName", ServerConnectionId = null, - Profiles = new Dictionary - { - { Language.CSharp, new ApplicableQualityProfile { ProfileKey = "sonar way", ProfileTimestamp = DateTime.Parse("2020-02-25T08:57:54+0000") } } - } }; serializedProject = @"{ ""ServerUri"": ""http://xxx.www.zzz/yyy:9000"", ""ProjectKey"": ""MyProject Key"", - ""ProjectName"": ""projectName"", - ""Profiles"": { - ""CSharp"": { - ""ProfileKey"": ""sonar way"", - ""ProfileTimestamp"": ""2020-02-25T08:57:54Z"" - } - } + ""ProjectName"": ""projectName"" }"; } @@ -203,23 +193,6 @@ public void Load_FileExists_DeserializedProject() actual.Should().BeEquivalentTo(bindingJsonModel); } - [TestMethod] - public void Load_FileExists_ProjectWithNonUtcTimestamp_DeserializedProjectWithCorrectTimestampData() - { - const string utcDate = "2020-02-25T08:57:54Z"; - const string localDate = "2020-02-25T10:57:54+02:00"; - serializedProject = serializedProject.Replace(utcDate, localDate); - - MockFileExists(MockFilePath); - fileSystem.File.ReadAllText(MockFilePath).Returns(serializedProject); - - var actual = testSubject.Load(MockFilePath); - actual.Should().BeEquivalentTo(bindingJsonModel); - - var deserializedTimestamp = actual.Profiles[Language.CSharp].ProfileTimestamp.Value.ToUniversalTime(); - deserializedTimestamp.Should().Be(new DateTime(2020, 2, 25, 8, 57, 54)); - } - [TestMethod] public void DeleteBindingDirectory_ConfigFilePathNotExists_ReturnsFalseAndLogs() { diff --git a/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs b/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs deleted file mode 100644 index 649d00c813..0000000000 --- a/src/ConnectedMode.UnitTests/ProjectRootCalculatorTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; - -[TestClass] -public class ProjectRootCalculatorTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task CalculateBasedOnLocalPathAsync_StandaloneMode_ReturnsNull() - { - var testSubject = CreateTestSubject(out _, out var configurationProviderMock, out _); - configurationProviderMock.SetupGet(x => x.CurrentConfiguration).Returns(BindingConfiguration.Standalone); - - var result = await testSubject.CalculateBasedOnLocalPathAsync(@"c:\somepath", CancellationToken.None); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task CalculateBasedOnLocalPathAsync_ConnectedMode_ReturnsCorrectRoot() - { - const string projectKey = "projectKey"; - const string branch = "branch"; - - var testSubject = CreateTestSubject(out var sonarQubeServiceMock, out var configurationProviderMock, out var branchProviderMock); - configurationProviderMock - .SetupGet(x => x.CurrentConfiguration) - .Returns(BindingConfiguration.CreateBoundConfiguration( - new BoundServerProject("solution", projectKey, new ServerConnection.SonarQube(new Uri("http://localhost"))), - SonarLintMode.Connected, - "somedir")); - branchProviderMock - .Setup(x => x.GetServerBranchNameAsync(It.IsAny())) - .ReturnsAsync(branch); - sonarQubeServiceMock - .Setup(x => x.SearchFilesByNameAsync(projectKey, branch, "file.cs", CancellationToken.None)) - .ReturnsAsync(new []{@"dir\file.cs"}); - - var result = await testSubject.CalculateBasedOnLocalPathAsync(@"c:\root\dir\file.cs", CancellationToken.None); - - result.Should().Be(@"c:\root\"); // more extensive testing of the marching algorithm is done in PathHelper tests - } - - private ProjectRootCalculator CreateTestSubject(out Mock sonarQubeServiceMock, - out Mock activeSolutionBoundTracker, - out Mock statefulServerBranchProviderMock) - { - return new ProjectRootCalculator((sonarQubeServiceMock = new Mock(MockBehavior.Strict)).Object, - (activeSolutionBoundTracker = new Mock(MockBehavior.Strict)).Object, - (statefulServerBranchProviderMock = new Mock(MockBehavior.Strict)).Object); - } -} diff --git a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs b/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs deleted file mode 100644 index 87d0940e96..0000000000 --- a/src/ConnectedMode.UnitTests/QualityProfiles/QualityProfileUpdaterTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.QualityProfiles; - -[TestClass] -public class QualityProfileUpdaterTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() - => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - [DataRow(SonarLintMode.Standalone)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task UpdateBoundSolutionAsync_NotNewConnectedMode_DoesNotUpdateQP(SonarLintMode mode) - { - var configProvider = CreateConfigProvider(mode, CreateDefaultProject()); - var qpDownloader = new Mock(); - var runner = CreatePassthroughRunner(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Invocations.Should().BeEmpty(); - qpDownloader.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdateIsDoneThroughRunner() - { - var cancellationToken = CancellationToken.None; - var boundProject = CreateDefaultProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); - var qpDownloader = new Mock(); - SetUpDownloader(qpDownloader); - // Using a pass-through runner so we can test the action passed to the runner - var runner = CreatePassthroughRunner(cancellationToken); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - qpDownloader.Verify(x => x.UpdateAsync(boundProject, null, cancellationToken), Times.Once); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_NoUpdates_EventIsNotRaised() - { - var boundProject = CreateDefaultProject(); - var configProvider = CreateConfigProvider(SonarLintMode.Connected, boundProject); - var qpDownloader = new Mock(); - SetUpDownloader(qpDownloader, false); - // Using a pass-through runner so we can test the action passed to the runner - var runner = CreatePassthroughRunner(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - qpDownloader.Verify(x => x.UpdateAsync(boundProject, null, It.IsAny()), Times.Once); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_IsNewConnectedMode_UpdaterDoesNotCallDownloaderDirectly() - { - var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); - var qpDownloader = new Mock(); - // Here, we're not using a pass-through runner, so we're not expecting the - // downloader to be invoked - var runner = new Mock(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - - // The updater should not be calling the downloader directly, only via the runner - qpDownloader.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public void Dispose_RunnerIsDisposed() - { - var runner = new Mock(); - var testSubject = CreateTestSubject(runner: runner.Object); - runner.Invocations.Should().BeEmpty(); - - testSubject.Dispose(); - - runner.Verify(x => x.Dispose(), Times.Once); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_JobIsCancelled_EventIsNotRaised() - { - // Simulate the runner cancelling a task - var cts = new CancellationTokenSource(); - cts.Cancel(); - var runner = new Mock(); - runner.Setup(x => x.RunAsync(It.IsAny>())) - .Callback>(_ => cts.Token.ThrowIfCancellationRequested()); - - var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); - var qpDownloader = new Mock(); - - var testSubject = CreateTestSubject(configProvider.Object, qpDownloader.Object, runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - cts.Dispose(); - } - - [TestMethod] - public async Task UpdateBoundSolutionAsync_InvalidOperationException_EventIsNotRaised() - { - var runner = new Mock(); - runner - .Setup(x => - x.RunAsync(It.IsAny>())) - .Throws(new InvalidOperationException()); - - var configProvider = CreateConfigProvider(SonarLintMode.Connected, CreateDefaultProject()); - - var testSubject = CreateTestSubject(configProvider.Object, runner: runner.Object); - - await testSubject.UpdateAsync(); - - configProvider.Verify(x => x.GetConfiguration(), Times.Once); - runner.Verify(x => x.RunAsync(It.IsAny>()), Times.Once); - } - - private static void SetUpDownloader(Mock qpDownloader, bool result = true) - { - qpDownloader - .Setup(x => - x.UpdateAsync(It.IsAny(), - It.IsAny>(), - It.IsAny())) - .ReturnsAsync(result); - } - - private BoundServerProject CreateDefaultProject() => new("solution", "project", new ServerConnection.SonarCloud("org")); - - private Mock CreateConfigProvider(SonarLintMode mode, BoundServerProject boundProject) - { - var config = new BindingConfiguration(boundProject, mode, "any directory"); - - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(config); - - return configProvider; - } - - private static Mock CreatePassthroughRunner(CancellationToken? token = null) - { - // If we want to test any of the code in the action passed to the runner we need - // to configure the runner to call it - var runner = new Mock(); - runner - .Setup(x => x.RunAsync(It.IsAny>())) - .Callback((Func callback) => callback(token ?? CancellationToken.None)); - - return runner; - } - - private static QualityProfileUpdater CreateTestSubject( - IConfigurationProvider configProvider = null, - IQualityProfileDownloader qpDownloader = null, - ICancellableActionRunner runner = null) - => new( - configProvider, - qpDownloader, - runner ?? CreatePassthroughRunner().Object, - new TestLogger(logToConsole: true)); -} diff --git a/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs b/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs deleted file mode 100644 index 01478f4035..0000000000 --- a/src/ConnectedMode.UnitTests/ServerQueryInfoProviderTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests -{ - [TestClass] - public class ServerQueryInfoProviderTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - [DataRow(SonarLintMode.Connected)] - [DataRow(SonarLintMode.LegacyConnected)] - public async Task Get_IsConnectedMode_ReturnsExpectedValues(SonarLintMode mode) - { - // Happy path - in connected mode, has branch - var config = CreateBindingConfig(mode, "my project key"); - var configProvider = CreateConfigProvider(config); - - var branchProvider = CreateBranchProvider("my branch"); - - var testSubject = CreateTestSubject(configProvider.Object, branchProvider.Object); - - var actual = await testSubject.GetProjectKeyAndBranchAsync(CancellationToken.None); - - actual.projectKey.Should().Be("my project key"); - actual.branchName.Should().Be("my branch"); - } - - [TestMethod] - public async Task Get_IsStandaloneMode_ReturnsNulls() - { - var config = CreateBindingConfig(SonarLintMode.Standalone, "my project key"); - var configProvider = CreateConfigProvider(config); - - var testSubject = CreateTestSubject(configProvider.Object); - - var actual = await testSubject.GetProjectKeyAndBranchAsync(CancellationToken.None); - - actual.projectKey.Should().BeNull(); - actual.branchName.Should().BeNull(); - } - - [TestMethod] - public void Get_OperationIsCancelled_ThrowsOperationCancelledException() - { - // Tests that the class doesn't squash OperationCancelledExceptions - // i.e. they are the callers responsibility - - var config = CreateBindingConfig(SonarLintMode.Connected, "my project key"); - var configProvider = CreateConfigProvider(config); - - var cancellationTokenSource = new CancellationTokenSource(); - - var branchProvider = new Mock(); - branchProvider.Setup(x => x.GetServerBranchNameAsync(cancellationTokenSource.Token)). - Callback(() => - { - // Simulate what happens when the cancellation token is cancelled - cancellationTokenSource.Cancel(); - cancellationTokenSource.Token.ThrowIfCancellationRequested(); - }); - - var testSubject = CreateTestSubject(configProvider.Object, branchProvider.Object); - - Func operation = () => testSubject.GetProjectKeyAndBranchAsync(cancellationTokenSource.Token); - - operation.Should().Throw(); - } - - - private static ServerQueryInfoProvider CreateTestSubject(IConfigurationProvider configurationProvider = null, - IStatefulServerBranchProvider serverBranchProvider = null) - { - configurationProvider ??= Mock.Of(); - serverBranchProvider ??= Mock.Of(); - - var testSubject = new ServerQueryInfoProvider(configurationProvider, serverBranchProvider); - return testSubject; - } - - private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), mode, "any dir"); - - private static Mock CreateConfigProvider(BindingConfiguration config = null) - { - config ??= CreateBindingConfig(); - - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(config); - return configProvider; - } - - private static Mock CreateBranchProvider(string branchName) - => CreateBranchProvider(branchName, CancellationToken.None); - - private static Mock CreateBranchProvider(string branchName, - CancellationToken cancellationToken) - { - var mock = new Mock(); - mock.Setup(x => x.GetServerBranchNameAsync(cancellationToken)).ReturnsAsync(branchName); - return mock; - } - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs deleted file mode 100644 index 16ebaf9cd6..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventChannelTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents.QualityProfile; - -[TestClass] -public class QualityProfileServerEventChannelTests -{ - [TestMethod] - public void MefCtor_CheckExportsMultipleInterfacesButSingleton() - { - MefTestHelpers.CheckMultipleExportsReturnSameInstance(); - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs deleted file mode 100644 index 5c89be3827..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/QualityProfile/QualityProfileServerEventsListenerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents.QualityProfile; - -[TestClass] -public class QualityProfileServerEventsListenerTests -{ - [TestMethod] - public void MefCtor_CheckExports() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public async Task ListenAsync_CallsUpdaterForEachEventUntilNullEvent() - { - var events = new[] - { - Task.FromResult(Mock.Of()), - Task.FromResult(Mock.Of()), - Task.FromResult(Mock.Of()), - Task.FromResult(null), // should stop on null no matter what - Task.FromResult(Mock.Of()), - }; - var eventSourceMock = SetUpEventSourceSequence(events); - var threadHandlingMock = SetUpThreadHandlingSwitchToBackgroundThread(); - var updaterMock = new Mock(); - - var testSubject = new QualityProfileServerEventsListener(eventSourceMock.Object, updaterMock.Object, threadHandlingMock.Object); - - await testSubject.ListenAsync(); - - eventSourceMock.Verify(x => x.GetNextEventOrNullAsync(), Times.Exactly(events.Length - 1 /* the event after null is ignored */)); - updaterMock.Verify(x => x.UpdateAsync(), Times.Exactly(events.Length - 1/* null doesn't trigger an update */ - 1/* the event after null is ignored */)); - threadHandlingMock.Verify(th => th.SwitchToBackgroundThread(), Times.Once); - } - - private static Mock SetUpThreadHandlingSwitchToBackgroundThread() - { - var threadHandlingMock = new Mock(); - threadHandlingMock.Setup(th => th.SwitchToBackgroundThread()).Returns(new NoOpThreadHandler.NoOpAwaitable()); - return threadHandlingMock; - } - - private static Mock SetUpEventSourceSequence(Task[] events) - { - var currentEventIndex = 0; - var eventSourceMock = new Mock(); - eventSourceMock.Setup(x => x.GetNextEventOrNullAsync()).Returns(() => events[currentEventIndex++]); - return eventSourceMock; - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs deleted file mode 100644 index 9eb7bccdba..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionFactoryTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; - -[TestClass] -public class SSESessionFactoryTests -{ - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Create_ReturnsCorrectType() - { - var testSubject = CreateTestSubject(); - - var sseSession = testSubject.Create("MyProjectName", null); - - sseSession.Should().NotBeNull().And.BeOfType(); - } - - [TestMethod] - public void Create_AfterDispose_Throws() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - Action act = () => testSubject.Create("MyProjectName", null); - - act.Should().Throw(); - } - - [TestMethod] - public void Dispose_IdempotentAndDisposesPublishers() - { - var issuesPublisherMock = new Mock(); - var qualityProfilePublisherMock = new Mock(); - var testSubject = CreateTestSubject(issuesPublisherMock, qualityProfilePublisherMock); - - testSubject.Dispose(); - testSubject.Dispose(); - testSubject.Dispose(); - - issuesPublisherMock.Verify(p => p.Dispose(), Times.Once); - qualityProfilePublisherMock.Verify(p => p.Dispose(), Times.Once); - } - - private SSESessionFactory CreateTestSubject(Mock issuePublisher = null, - Mock qualityProfileServerEventSourcePublisher = null) => - new(Mock.Of(), - issuePublisher?.Object ?? Mock.Of(), - qualityProfileServerEventSourcePublisher?.Object ?? Mock.Of(), - Mock.Of(), - Mock.Of()); -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs deleted file mode 100644 index 1a02131ed2..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionManagerTests.cs +++ /dev/null @@ -1,340 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; - -[TestClass] -public class SSESessionManagerTests -{ - private const string DefaultProjectKey = "myproj"; - - [TestMethod] - public void MefCtor_CheckIsExported() - { - var activeSolutionBoundTrackerMock = new Mock(); - activeSolutionBoundTrackerMock.SetupGet(tracker => tracker.CurrentConfiguration) - .Returns(BindingConfiguration.Standalone); - - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(activeSolutionBoundTrackerMock.Object), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Ctor_DoesNotCallAnyServices_BesidesExpected() - { - var activeSolutionBoundTracker = new Mock(); - var sseSessionFactory = new Mock(); - var logger = new Mock(); - - var _ = new SSESessionManager(activeSolutionBoundTracker.Object, sseSessionFactory.Object, logger.Object); - - // The MEF constructor should be free-threaded, which it will be if - // it doesn't make any external calls. - - activeSolutionBoundTracker.VerifyAdd(tracker => tracker.SolutionBindingChanged += It.IsAny>(), Times.Once); - activeSolutionBoundTracker.VerifyNoOtherCalls(); - sseSessionFactory.Invocations.Should().BeEmpty(); - logger.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_SubscribesToBindingChangedEvent() - { - var testScope = new TestScope(); - - var _ = testScope.CreateTestSubject(); - - testScope.ActiveSolutionBoundTrackerMock.VerifyAdd( - tracker => tracker.SolutionBindingChanged += - It.IsAny>(), Times.Once); - } - - [TestMethod] - public void CreateSessionIfInConnectedMode_WhenInStandaloneModeOnCreation_DoesNotCreateSession() - { - var bindingConfig = BindingConfiguration.Standalone; - var testScope = new TestScope(bindingConfig); - - var _ = testScope.CreateTestSubject(); - var testSubject = testScope.CreateTestSubject(); - - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Never); - } - - [TestMethod] - public void CreateSessionIfInConnectedMode_WhenConnectedToSonarCloud_DoesNotCreateSession() - { - var bindingConfig = TestScope.CreateConnectedModeSonarCloudBindingConfiguration(DefaultProjectKey); - - var testScope = new TestScope(bindingConfig); - - var _ = testScope.CreateTestSubject(); - var testSubject = testScope.CreateTestSubject(); - - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Never); - } - - [TestMethod] - public void CreateSessionIfInConnectedMode_WhenInConnectedModeOnCreation_CreatesSession() - { - var bindingConfig = TestScope.CreateConnectedModeSonarQubeBindingConfiguration(DefaultProjectKey); - - var testScope = new TestScope(bindingConfig); - var sessionMock = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - - var testSubject = testScope.CreateTestSubject(); - - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Once); - sessionMock.Verify(session => session.PumpAllAsync(), Times.Once); - } - - [TestMethod] - public void OnSolutionChanged_WhenChangesFromStandaloneToConnected_CreatesSessionAndLaunchesIt() - { - var testScope = new TestScope(); - var _ = testScope.CreateTestSubject(); - testScope.RaiseInStandaloneModeEvent(); - var sessionMock = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - - testScope.RaiseInConnectedModeEvent(DefaultProjectKey); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Once); - sessionMock.Verify(session => session.PumpAllAsync(), Times.Once); - } - - [TestMethod] - public void OnSolutionChanged_WhenChangesFromConnectedToStandalone_DisposesPreviousSession() - { - var testScope = new TestScope(); - var _ = testScope.CreateTestSubject(); - var sessionMock = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - sessionMock.Setup(session => session.Dispose()); - testScope.RaiseInConnectedModeEvent(DefaultProjectKey); - testScope.SSESessionFactoryMock.Invocations.Clear(); - - testScope.RaiseInStandaloneModeEvent(); - - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(It.IsAny(), It.IsAny()), Times.Never); - sessionMock.Verify(session => session.Dispose(), Times.Once); - } - - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public void OnSolutionChanged_WhenChangesFromConnectedToConnected_CancelsSessionAndStartsNewOne(bool sameProjectKey) - { - var projectKey1 = "proj1"; - var projectKey2 = sameProjectKey ? projectKey1 : "proj2"; - var testScope = new TestScope(); - var _ = testScope.CreateTestSubject(); - var sessionMock1 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(projectKey1); - sessionMock1.Setup(session => session.Dispose()); - testScope.RaiseInConnectedModeEvent(projectKey1); - var sessionMock2 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(projectKey2); - - testScope.RaiseInConnectedModeEvent(projectKey2); - - sessionMock1.Verify(session => session.Dispose(), Times.Once); - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(projectKey2, It.IsAny()), Times.Exactly(sameProjectKey ? 2 : 1)); - sessionMock2.Verify(session => session.Dispose(), Times.Never); - } - - [DataTestMethod] - public async Task OnSessionFailed_CancelsSessionAndStartsNewOne() - { - var bindingConfig = TestScope.CreateConnectedModeSonarQubeBindingConfiguration(DefaultProjectKey); - - var testScope = new TestScope(bindingConfig); - - var sessionMock1 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - sessionMock1.Setup(session => session.Dispose()); - var testSubject = testScope.CreateTestSubject(); - testSubject.CreateSessionIfInConnectedMode(bindingConfig); - - var sessionMock2 = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - - await testScope.CapturedSessionFailedCallback(sessionMock1.Object); - - sessionMock1.Verify(session => session.Dispose(), Times.Once); - testScope.SSESessionFactoryMock.Verify(factory => factory.Create(DefaultProjectKey, It.IsAny()), Times.Exactly(2)); - sessionMock2.Verify(session => session.Dispose(), Times.Never); - } - - [TestMethod] - public void Dispose_WhenInConnectedMode_CorrectAndIdempotent() - { - var testScope = new TestScope(); - var testSubject = testScope.CreateTestSubject(); - var sseSession = testScope.SetUpSSEFactoryToReturnNoOpSSESession(DefaultProjectKey); - testScope.SetUpCorrectDisposeOrder(sseSession); - testScope.RaiseInConnectedModeEvent(DefaultProjectKey); - - CallDisposeMultipleTimes(testSubject); - - VerifyUnsubscribedFromBindingChangedEvent(testScope); - VerifySSESessionFactoryDisposedOnce(testScope); - sseSession.Verify(session => session.Dispose(), Times.Once); - } - - [TestMethod] - public void Dispose_WhenInStandaloneMode_CorrectAndIdempotent() - { - var testScope = new TestScope(); - var testSubject = testScope.CreateTestSubject(); - testScope.SetUpCorrectDisposeOrder(null); - testScope.RaiseInStandaloneModeEvent(); - - CallDisposeMultipleTimes(testSubject); - - VerifyUnsubscribedFromBindingChangedEvent(testScope); - VerifySSESessionFactoryDisposedOnce(testScope); - } - - private static void VerifySSESessionFactoryDisposedOnce(TestScope testScope) - { - testScope.SSESessionFactoryMock.Verify(sessionFactory => sessionFactory.Dispose(), Times.Once); - } - - private static void VerifyUnsubscribedFromBindingChangedEvent(TestScope testScope) - { - testScope.ActiveSolutionBoundTrackerMock.VerifyRemove( - tracker => - tracker.SolutionBindingChanged -= - It.IsAny>(), - Times.Once); - } - - private static void CallDisposeMultipleTimes(SSESessionManager testSubject) - { - testSubject.Dispose(); - testSubject.Dispose(); - testSubject.Dispose(); - } - - private class TestScope - { - private readonly MockRepository mockRepository; - private readonly MockSequence callOrder = new(); - - public TestScope(BindingConfiguration initialBindingState = null) - { - mockRepository = new MockRepository(MockBehavior.Strict); - ActiveSolutionBoundTrackerMock = mockRepository.Create(); - SSESessionFactoryMock = mockRepository.Create(); - - // This is not in a sequence so we can call it multiple times - ActiveSolutionBoundTrackerMock - .SetupGet(tracker => tracker.CurrentConfiguration) - .Returns(initialBindingState ?? BindingConfiguration.Standalone); - } - - public Mock ActiveSolutionBoundTrackerMock { get; } - public Mock SSESessionFactoryMock { get; } - public OnSessionFailedAsync CapturedSessionFailedCallback { get; private set; } - - public SSESessionManager CreateTestSubject() - { - return new SSESessionManager( - ActiveSolutionBoundTrackerMock.Object, - SSESessionFactoryMock.Object, - Mock.Of()); - } - - public static BindingConfiguration CreateConnectedModeSonarQubeBindingConfiguration(string projectKey) - { - var sonarQube = new ServerConnection.SonarQube(new Uri("http://localhost")); - return CreateConnectedModeBindingConfiguration(projectKey, sonarQube); - } - - public static BindingConfiguration CreateConnectedModeSonarCloudBindingConfiguration(string projectKey) - { - var sonarCloud = new ServerConnection.SonarCloud(projectKey); - return CreateConnectedModeBindingConfiguration(projectKey, sonarCloud); - } - - private static BindingConfiguration CreateConnectedModeBindingConfiguration(string projectKey, ServerConnection serverConnection) - { - var randomString = Guid.NewGuid().ToString(); - var bindingConfiguration = new BindingConfiguration( - new BoundServerProject(randomString, projectKey, serverConnection), - SonarLintMode.Connected, - randomString); - return bindingConfiguration; - } - - public void SetUpCorrectDisposeOrder(Mock currentSession) - { - ActiveSolutionBoundTrackerMock.SetupRemove(tracker => - tracker.SolutionBindingChanged -= It.IsAny>()); - - SSESessionFactoryMock.InSequence(callOrder).Setup(sessionFactory => sessionFactory.Dispose()); - - currentSession?.InSequence(callOrder).Setup(session => session.Dispose()); - } - - public Mock SetUpSSEFactoryToReturnNoOpSSESession(string projectKey, Action factoryMockCallback = null) - { - var sseSessionMock = mockRepository.Create(); - - SSESessionFactoryMock.InSequence(callOrder) - .Setup(sessionFactory => sessionFactory.Create(projectKey, It.IsAny())) - .Returns(sseSessionMock.Object) - .Callback((string projectKey,OnSessionFailedAsync callbackAction) => { - CapturedSessionFailedCallback = callbackAction; - factoryMockCallback?.Invoke(); - }); - - sseSessionMock - .InSequence(callOrder) - .Setup(session => session.PumpAllAsync()) - .Returns(Task.CompletedTask); - - return sseSessionMock; - } - - public void RaiseInConnectedModeEvent(string projectKey) - { - var openProjectEvent = CreateConnectedModeSonarQubeBindingConfiguration(projectKey); - RaiseSolutionBindingEvent(openProjectEvent); - } - - public void RaiseInStandaloneModeEvent() - { - RaiseSolutionBindingEvent(BindingConfiguration.Standalone); - } - - private void RaiseSolutionBindingEvent(BindingConfiguration bindingConfiguration) - { - ActiveSolutionBoundTrackerMock.Raise(tracker => tracker.SolutionBindingChanged += null, new ActiveSolutionBindingEventArgs(bindingConfiguration)); - } - } -} diff --git a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs b/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs deleted file mode 100644 index 55cc3dd44e..0000000000 --- a/src/ConnectedMode.UnitTests/ServerSentEvents/SSESessionTests.cs +++ /dev/null @@ -1,288 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; - -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests.ServerSentEvents; - -[TestClass] -public class SSESessionTests -{ - [TestMethod] - public void PumpAllAsync_WhenSonarQubeRefusesConnection_DoesNotThrow() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - testScope.SonarQubeServiceMock - .InSequence(testScope.CallOrder) - .Setup(sqs => sqs.CreateSSEStreamReader(It.IsAny(), It.IsAny())) - .ReturnsAsync((ISSEStreamReader)null); - - Func act = () => testScope.TestSubject.PumpAllAsync(); - - act.Should().NotThrow(); - } - - - [TestMethod] - public async Task PumpAllAsync_SelectsPublisherCorrectlyAndPreservesOrderWithinType() - { - var testScope = new TestScope(); - var inputSequence = new IServerEvent[] - { - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - }; - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, inputSequence); - - await testScope.TestSubject.PumpAllAsync(); - - CheckEventsSequence(testScope.IssuePublisherMock.Invocations); - CheckEventsSequence(testScope.QualityProfilePublisherMock.Invocations); - - // note: can't pass Mock> because Mock's generic type is not covariant and we use the publisher interface through inheritor interfaces - void CheckEventsSequence(IEnumerable publisherMockInvocations) where T : class, IServerEvent - { - var publishedEvents = publisherMockInvocations.Select(call => call.Arguments.First() as T); - var expectedPublishedEvents = inputSequence.Where(issuesEvent => issuesEvent is T); - - publishedEvents.Should().BeEquivalentTo(expectedPublishedEvents); - } - } - - [TestMethod] - public async Task PumpAllAsync_WhenNullEvent_Ignores() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, - new IServerEvent[] - { - Mock.Of(), - null, - Mock.Of(), - Mock.Of(), - Mock.Of() - }); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.IssuePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); - testScope.QualityProfilePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(2)); - } - - [TestMethod] - public async Task PumpAllAsync_WhenUnsupportedEvent_Ignores() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - - testScope.SetUpSSEStreamReaderToReturnEventsSequenceAndExit(sseStreamMock, - new IServerEvent[] - { - Mock.Of(), - Mock.Of(), - Mock.Of() - }); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.IssuePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(1)); - testScope.QualityProfilePublisherMock.Verify(publisher => publisher.Publish(It.IsAny()), Times.Exactly(1)); - } - - [TestMethod] - public async Task PumpAllAsync_WhenPublisherErrors_NonCriticalError_ErrorLoggedAndSessionIsDisposed() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - sseStreamMock.Setup(x => x.ReadAsync()).Throws(new NotImplementedException("this is a test")); - - testScope.LoggerMock.Setup(x => x.LogVerbose(It.Is(s => s.Contains("this is a test")), Array.Empty())); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.LoggerMock.Verify(x=> x.LogVerbose(It.Is(s=> s.Contains("this is a test")), Array.Empty()), Times.Once); - testScope.CapturedSessionToken.Value.IsCancellationRequested.Should().BeTrue(); - } - - [TestMethod] - public void PumpAllAsync_WhenPublisherErrors_CriticalError_SessionThrows() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - sseStreamMock.Setup(x => x.ReadAsync()).Throws(new DivideByZeroException("this is a test")); - - Func func = async () => await testScope.TestSubject.PumpAllAsync(); - - func.Should().ThrowExactly().And.Message.Should().Be("this is a test"); - testScope.LoggerMock.Invocations.Count.Should().Be(0); - testScope.CapturedSessionToken.Value.IsCancellationRequested.Should().BeFalse(); - } - - [TestMethod] - public void PumpAllAsync_AfterDisposed_Throws() - { - var testScope = new TestScope(); - testScope.TestSubject.Dispose(); - - var act = () => testScope.TestSubject.PumpAllAsync(); - - act.Should().Throw(); - } - - [TestMethod] - public async Task Dispose_FinishesSession() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - var readTcs = new TaskCompletionSource(); - sseStreamMock.Setup(x => x.ReadAsync()).Returns(readTcs.Task); - - var pumpTask = testScope.TestSubject.PumpAllAsync(); - testScope.TestSubject.Dispose(); - readTcs.SetResult(null); - await pumpTask; - - var sessionToken = testScope.CapturedSessionToken; - sessionToken.Should().NotBeNull(); - sessionToken.Value.IsCancellationRequested.Should().BeTrue(); - } - - [TestMethod] - public async Task NonCriticalException_InvokesOnSessionFailedAsync() - { - var testScope = new TestScope(); - testScope.SetUpSwitchToBackgroundThread(); - - var sseStreamMock = testScope.SetUpSQServiceToSuccessfullyReturnSSEStreamReader(); - sseStreamMock.Setup(x => x.ReadAsync()).Throws(new NotImplementedException("this is a test")); - - await testScope.TestSubject.PumpAllAsync(); - - testScope.OnSessionFailedAsyncMock.Invocations.Should().HaveCount(1); - } - - public interface IDummyServerEvent : IServerEvent { } - - private static Mock GetOnSessionFailedAsyncMock() - { - var createOp = new Mock(); - createOp.Setup(x => x.Invoke(It.IsAny())); - return createOp; - } - - private class TestScope - { - private readonly MockRepository mockRepository; - - public TestScope() - { - mockRepository = new MockRepository(MockBehavior.Strict); - SonarQubeServiceMock = mockRepository.Create(); - IssuePublisherMock = mockRepository.Create(MockBehavior.Loose); - QualityProfilePublisherMock = mockRepository.Create(MockBehavior.Loose); - ThreadHandlingMock = mockRepository.Create(); - LoggerMock = new Mock(); - OnSessionFailedAsyncMock = GetOnSessionFailedAsyncMock(); - - var factory = new SSESessionFactory( - SonarQubeServiceMock.Object, - IssuePublisherMock.Object, - QualityProfilePublisherMock.Object, - ThreadHandlingMock.Object, - LoggerMock.Object); - - TestSubject = factory.Create("blalala", OnSessionFailedAsyncMock.Object); - } - - private Mock ThreadHandlingMock { get; } - public Mock SonarQubeServiceMock { get; } - public Mock IssuePublisherMock { get; } - public Mock QualityProfilePublisherMock { get; } - public Mock LoggerMock { get; } - public CancellationToken? CapturedSessionToken { get; private set; } - public MockSequence CallOrder { get; } = new MockSequence(); - public ISSESession TestSubject { get; } - public Mock OnSessionFailedAsyncMock { get; } - - public void SetUpSSEStreamReaderToReturnEventsSequenceAndExit(Mock sseStreamMock, IServerEvent[] inputSequence) - { - foreach (var serverEvent in inputSequence) - { - sseStreamMock - .InSequence(CallOrder) - .Setup(r => r.ReadAsync()) - .ReturnsAsync(serverEvent); - } - - sseStreamMock - .InSequence(CallOrder) - .Setup(r => r.ReadAsync()) - .ReturnsAsync(() => - { - TestSubject.Dispose(); - return null; - }); - } - - public Mock SetUpSQServiceToSuccessfullyReturnSSEStreamReader() - { - var sseStreamMock = mockRepository.Create(); - - SonarQubeServiceMock - .InSequence(CallOrder) - .Setup(client => client.CreateSSEStreamReader(It.IsAny(), - It.Is(token => token != CancellationToken.None))) - .ReturnsAsync((string _, CancellationToken tokenArg) => - { - CapturedSessionToken = tokenArg; - return sseStreamMock.Object; - }); - - return sseStreamMock; - } - - public void SetUpSwitchToBackgroundThread() - { - ThreadHandlingMock - .InSequence(CallOrder) - .Setup(th => th.SwitchToBackgroundThread()) - .Returns(new NoOpThreadHandler.NoOpAwaitable()); - } - } -} diff --git a/src/ConnectedMode/Binding/BoundSonarQubeProject.cs b/src/ConnectedMode/Binding/BoundSonarQubeProject.cs index abcc8c4b26..73f3f103ee 100644 --- a/src/ConnectedMode/Binding/BoundSonarQubeProject.cs +++ b/src/ConnectedMode/Binding/BoundSonarQubeProject.cs @@ -59,7 +59,6 @@ public BoundSonarQubeProject( public string ProjectKey { get; set; } public string ProjectName { get; set; } - public Dictionary Profiles { get; set; } [JsonIgnore] public IConnectionCredentials Credentials { get; set; } diff --git a/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs b/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs index c9acb15b7e..2c00cafcb0 100644 --- a/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs +++ b/src/ConnectedMode/Binding/BoundSonarQubeProjectExtensions.cs @@ -36,7 +36,7 @@ public static ServerConnection FromBoundSonarQubeProject(this BoundSonarQubeProj public static BoundServerProject FromBoundSonarQubeProject(this BoundSonarQubeProject boundProject, string localBindingKey, ServerConnection connection) => new(localBindingKey ?? throw new ArgumentNullException(nameof(localBindingKey)), boundProject?.ProjectKey ?? throw new ArgumentNullException(nameof(boundProject)), - connection ?? throw new ArgumentNullException(nameof(connection))) { Profiles = boundProject.Profiles }; + connection ?? throw new ArgumentNullException(nameof(connection))); public static ConnectionInformation CreateConnectionInformation(this BoundSonarQubeProject binding) { diff --git a/src/ConnectedMode/Binding/SonarQubeRuleStatus.cs b/src/ConnectedMode/Binding/SonarQubeRuleStatus.cs deleted file mode 100644 index 28fbd16b4d..0000000000 --- a/src/ConnectedMode/Binding/SonarQubeRuleStatus.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode.Binding; - -public class SonarQubeRoslynRuleStatus(SonarQubeRule sonarQubeRule, IEnvironmentSettings environmentSettings) : IRoslynRuleStatus -{ - public string Key => sonarQubeRule.Key; - - public RuleAction GetSeverity() - { - if (!sonarQubeRule.IsActive) - { - return RuleAction.None; - } - return GetVsSeverity(sonarQubeRule.SoftwareQualitySeverities?.Values) ?? GetVsSeverity(sonarQubeRule.Severity); - } - - private RuleAction? GetVsSeverity(ICollection severities) => - severities is not { Count: > 0 } - ? null - : GetVsSeverity(severities.Max()); - - private RuleAction GetVsSeverity(SonarQubeSoftwareQualitySeverity severity) => - severity switch - { - SonarQubeSoftwareQualitySeverity.Blocker or SonarQubeSoftwareQualitySeverity.High => environmentSettings.TreatBlockerSeverityAsError() ? RuleAction.Error : RuleAction.Warning, - SonarQubeSoftwareQualitySeverity.Medium => RuleAction.Warning, - SonarQubeSoftwareQualitySeverity.Low or SonarQubeSoftwareQualitySeverity.Info => RuleAction.Info, - _ => throw new ArgumentOutOfRangeException($"Unsupported SonarQube issue severity: {severity}") - }; - - private RuleAction GetVsSeverity(SonarQubeIssueSeverity sqSeverity) => - sqSeverity switch - { - SonarQubeIssueSeverity.Info or SonarQubeIssueSeverity.Minor => RuleAction.Info, - SonarQubeIssueSeverity.Major or SonarQubeIssueSeverity.Critical => RuleAction.Warning, - SonarQubeIssueSeverity.Blocker => environmentSettings.TreatBlockerSeverityAsError() ? RuleAction.Error : RuleAction.Warning, - _ => throw new ArgumentOutOfRangeException($"Unsupported SonarQube issue severity: {sqSeverity}") - }; -} diff --git a/src/ConnectedMode/ConnectedMode.csproj b/src/ConnectedMode/ConnectedMode.csproj index e0affdf10b..6e4c445304 100644 --- a/src/ConnectedMode/ConnectedMode.csproj +++ b/src/ConnectedMode/ConnectedMode.csproj @@ -129,11 +129,6 @@ True PersistenceStrings.resx - - QualityProfilesStrings.resx - True - True - True True @@ -159,11 +154,6 @@ ResXFileCodeGenerator PersistenceStrings.Designer.cs - - Designer - QualityProfilesStrings.Designer.cs - ResXFileCodeGenerator - ResXFileCodeGenerator Resources.Designer.cs diff --git a/src/ConnectedMode/ConnectedModePackage.cs b/src/ConnectedMode/ConnectedModePackage.cs index 53f31f7aae..a27a91241d 100644 --- a/src/ConnectedMode/ConnectedModePackage.cs +++ b/src/ConnectedMode/ConnectedModePackage.cs @@ -24,8 +24,6 @@ using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.ConnectedMode.Hotspots; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using Task = System.Threading.Tasks.Task; @@ -38,8 +36,6 @@ namespace SonarLint.VisualStudio.ConnectedMode [Guid("dd3427e0-7bb2-4a51-b00a-ddae2c32c7ef")] public sealed class ConnectedModePackage : AsyncPackage { - private SSESessionManager sseSessionManager; - private IQualityProfileServerEventsListener qualityProfileServerEventsListener; private IHotspotDocumentClosedHandler hotspotDocumentClosedHandler; private IHotspotSolutionClosedHandler hotspotSolutionClosedHandler; private ILocalHotspotStoreMonitor hotspotStoreMonitor; @@ -53,11 +49,6 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke logger.WriteLine(Resources.Package_Initializing); - LoadServicesAndDoInitialUpdates(componentModel); - - qualityProfileServerEventsListener = componentModel.GetService(); - qualityProfileServerEventsListener.ListenAsync().Forget(); - hotspotDocumentClosedHandler = componentModel.GetService(); hotspotSolutionClosedHandler = componentModel.GetService(); @@ -68,25 +59,5 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke logger.WriteLine(Resources.Package_Initialized); } - /// - /// Trigger an initial update of classes that need them. (These classes might have missed the initial solution binding - /// event from the ActiveSolutionBoundTracker) - /// See https://github.com/SonarSource/sonarlint-visualstudio/issues/3886 - /// - private void LoadServicesAndDoInitialUpdates(IComponentModel componentModel) - { - sseSessionManager = componentModel.GetService(); - sseSessionManager.CreateSessionIfInConnectedMode(); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - sseSessionManager?.Dispose(); - } - - base.Dispose(disposing); - } } } diff --git a/src/ConnectedMode/IssueMatcher.cs b/src/ConnectedMode/IssueMatcher.cs deleted file mode 100644 index 2e5e768972..0000000000 --- a/src/ConnectedMode/IssueMatcher.cs +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Linq; -using SonarLint.VisualStudio.Core.Helpers; -using SonarLint.VisualStudio.Core.Suppressions; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - internal interface IIssueMatcher - { - /// - /// This method attempts to match with . - /// - /// There's a possibility of False Positive matches: in case false tail-match of file paths, as the project root is not taken into account, - /// or when line number matches but line hash doesn't. - /// - /// Local issue - /// Server issue - /// The best possible match based on rule id, file name (tail matched to the server path), line number and line hash (not checked when line numbers match) - bool IsLikelyMatch(IFilterableIssue issue, SonarQubeIssue serverIssue); - - /// - /// Returns the first likely matching issue. - /// - /// For this method to work correctly, all ; need to be from the same server file. - /// False Positives are possible, since <see cref="IsLikelyMatch"/> can return true for multiple issues in the same file and only the firs one is returned. - /// - /// Local issue - /// List of server issues from the same file - /// A matching server issue, if present in the list, or null - SonarQubeIssue GetFirstLikelyMatchFromSameFileOrNull(IFilterableIssue issue, IEnumerable serverIssuesFromSameFile); - } - - [Export(typeof(IIssueMatcher))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class IssueMatcher : IIssueMatcher - { - public bool IsLikelyMatch(IFilterableIssue issue, SonarQubeIssue serverIssue) - { - return IsMatch(issue, serverIssue, true); - } - - public SonarQubeIssue GetFirstLikelyMatchFromSameFileOrNull(IFilterableIssue issue, IEnumerable serverIssuesFromSameFile) - { - return serverIssuesFromSameFile?.FirstOrDefault(serverIssue => IsMatch(issue, serverIssue, false)); - } - - private static bool IsMatch(IFilterableIssue issue, SonarQubeIssue serverIssue, bool checkFilePath) - { - // File-level issues (i.e. line = null) match if: - // 1. Same component, same file, same error code. - - // Non-file-level issues match if: - // 1. Same component, same file, same error code, same line hash // tolerant to line number changing - // 2. Same component, same file, same error code, same line // tolerant to code on the line changing e.g. var rename - - // File-level issues never match non-file-level issues. - - if (!StringComparer.OrdinalIgnoreCase.Equals(issue.RuleId, serverIssue.RuleId)) - { - return false; - } - - if (checkFilePath && !PathHelper.IsServerFileMatch(issue.FilePath, serverIssue.FilePath)) - { - return false; - } - - // file level issue - if (IsFileLevelServerIssue(serverIssue)) - { - return CheckLocalIssueIsFileLevel(issue); - } - - // Non-file level issue - - return issue.StartLine == serverIssue.TextRange?.StartLine || CompareHash(issue, serverIssue); - } - - private static bool CheckLocalIssueIsFileLevel(IFilterableIssue issue) - { - return !issue.StartLine.HasValue - || (issue is IFilterableRoslynIssue roslynIssue // We don't know the end of the issue location, so this is our best guess. - && roslynIssue.RoslynStartLine == 1 - && roslynIssue.RoslynStartColumn == 1); // This check relies on the fact that a roslyn file-level issue - // always starts at the beginning of the file - // and the fact that the rule can't be both file-level and not. - // See SuppressionChecker.IsSameLine for an example of a solution w/o false-positives - } - - private static bool IsFileLevelServerIssue(SonarQubeIssue serverIssue) - { - return serverIssue.TextRange == null; - } - - private static bool CompareHash(IFilterableIssue issue, SonarQubeIssue serverIssue) => - issue.LineHash != null - && serverIssue.Hash != null - && StringComparer.Ordinal.Equals(issue.LineHash, serverIssue.Hash); - } -} diff --git a/src/ConnectedMode/Persistence/BindingJsonModel.cs b/src/ConnectedMode/Persistence/BindingJsonModel.cs index 30da22b8a0..be86a25030 100644 --- a/src/ConnectedMode/Persistence/BindingJsonModel.cs +++ b/src/ConnectedMode/Persistence/BindingJsonModel.cs @@ -36,5 +36,4 @@ internal class BindingJsonModel public SonarQubeOrganization Organization { get; set; } // left here for backward compatibility reasons public string ProjectKey { get; set; } public string ProjectName { get; set; } // left here for backward compatibility reasons - public Dictionary Profiles { get; set; } } diff --git a/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs b/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs index 01168db482..858e6965df 100644 --- a/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs +++ b/src/ConnectedMode/Persistence/BindingJsonModelConverter.cs @@ -39,14 +39,13 @@ internal interface IBindingJsonModelConverter internal class BindingJsonModelConverter : IBindingJsonModelConverter { public BoundServerProject ConvertFromModel(BindingJsonModel bindingJsonModel, ServerConnection connection, string localBindingKey) => - new(localBindingKey, bindingJsonModel.ProjectKey, connection) { Profiles = bindingJsonModel.Profiles }; + new(localBindingKey, bindingJsonModel.ProjectKey, connection); public BindingJsonModel ConvertToModel(BoundServerProject binding) => new() { ProjectKey = binding.ServerProjectKey, ServerConnectionId = binding.ServerConnection.Id, - Profiles = binding.Profiles, // for compatibility reasons: ServerUri = binding.ServerConnection.ServerUri, Organization = binding.ServerConnection is ServerConnection.SonarCloud sonarCloudConnection @@ -59,5 +58,5 @@ public BoundSonarQubeProject ConvertFromModelToLegacy(BindingJsonModel bindingJs bindingJsonModel.ProjectKey, bindingJsonModel.ProjectName, credentials, - bindingJsonModel.Organization) { Profiles = bindingJsonModel.Profiles }; + bindingJsonModel.Organization); } diff --git a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs index dd9864229e..9402fe8f34 100644 --- a/src/ConnectedMode/Persistence/SolutionBindingRepository.cs +++ b/src/ConnectedMode/Persistence/SolutionBindingRepository.cs @@ -145,20 +145,8 @@ public IEnumerable List() } } - private BindingJsonModel ReadBindingFile(string configFilePath) - { - var bound = solutionBindingFileLoader.Load(configFilePath); - - if (bound is null) - { - return null; - } - - Debug.Assert(!bound.Profiles?.ContainsKey(Language.Unknown) ?? true, - "Not expecting the deserialized binding config to contain the profile for an unknown language"); - - return bound; - } + private BindingJsonModel ReadBindingFile(string configFilePath) => + solutionBindingFileLoader.Load(configFilePath); private BoundServerProject Convert(BindingJsonModel bindingJsonModel, string configFilePath) => bindingJsonModel is not null && serverConnectionsRepository.TryGet(bindingJsonModel.ServerConnectionId, out var connection) diff --git a/src/ConnectedMode/ProjectRootCalculator.cs b/src/ConnectedMode/ProjectRootCalculator.cs deleted file mode 100644 index 78171f6277..0000000000 --- a/src/ConnectedMode/ProjectRootCalculator.cs +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.Helpers; -using SonarQube.Client; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - internal interface IProjectRootCalculator - { - Task CalculateBasedOnLocalPathAsync(string localPath, CancellationToken token); - } - - [Export(typeof(IProjectRootCalculator))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class ProjectRootCalculator : IProjectRootCalculator - { - private readonly ISonarQubeService sonarQubeService; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IStatefulServerBranchProvider branchProvider; - - [ImportingConstructor] - public ProjectRootCalculator(ISonarQubeService sonarQubeService, IActiveSolutionBoundTracker activeSolutionBoundTracker, IStatefulServerBranchProvider branchProvider) - { - this.sonarQubeService = sonarQubeService; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.branchProvider = branchProvider; - } - - public async Task CalculateBasedOnLocalPathAsync(string localPath, CancellationToken token) - { - var bindingConfiguration = activeSolutionBoundTracker.CurrentConfiguration; - - if (bindingConfiguration.Mode == SonarLintMode.Standalone) - { - return null; - } - - return PathHelper.CalculateServerRoot(localPath, - await sonarQubeService.SearchFilesByNameAsync(bindingConfiguration.Project.ServerProjectKey, - await branchProvider.GetServerBranchNameAsync(token), - Path.GetFileName(localPath), - token)); - } - } -} diff --git a/src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs b/src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs deleted file mode 100644 index 79c2dc835e..0000000000 --- a/src/ConnectedMode/QualityProfiles/QualityProfileUpdater.cs +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.ConnectedMode.Helpers; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using Task = System.Threading.Tasks.Task; - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles -{ - internal interface IQualityProfileUpdater - { - /// - /// When in Connected Mode, ensures that all of the Quality Profiles are up to date - /// - Task UpdateAsync(); - } - - [Export(typeof(IQualityProfileUpdater))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class QualityProfileUpdater : IQualityProfileUpdater, IDisposable - { - private readonly IConfigurationProvider configProvider; - private readonly IQualityProfileDownloader qualityProfileDownloader; - private readonly ICancellableActionRunner runner; - private readonly ILogger logger; - - [ImportingConstructor] - public QualityProfileUpdater(IConfigurationProvider configProvider, - IQualityProfileDownloader qualityProfileDownloader, - ICancellableActionRunner runner, - ILogger logger) - { - this.configProvider = configProvider; - this.qualityProfileDownloader = qualityProfileDownloader; - this.runner = runner; - this.logger = logger; - } - - public async Task UpdateAsync() - { - var config = configProvider.GetConfiguration(); - if (config.Mode != SonarLintMode.Connected) - { - logger.LogVerbose($"[{nameof(QualityProfileUpdater)}] Skipping Quality Profile update. Solution is not bound. Mode: {config.Mode}"); - return; - } - - try - { - await runner.RunAsync(async token => await qualityProfileDownloader.UpdateAsync(config.Project, null, token)); - } - catch (Exception e) when (e is OperationCanceledException || e is InvalidOperationException) - { - logger.LogVerbose($"[{nameof(QualityProfileUpdater)}] {e}"); - } - } - - public void Dispose() => runner.Dispose(); - } -} diff --git a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs b/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs deleted file mode 100644 index f087ece6df..0000000000 --- a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.Designer.cs +++ /dev/null @@ -1,135 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class QualityProfilesStrings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal QualityProfilesStrings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonarLint.VisualStudio.ConnectedMode.QualityProfiles.QualityProfilesStrings", typeof(QualityProfilesStrings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Downloading quality profile: {0}. - /// - internal static string DownloadingQualityProfileProgressMessage { - get { - return ResourceManager.GetString("DownloadingQualityProfileProgressMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All quality profiles are up to date. - /// - internal static string DownloadingQualityProfilesNotNeeded { - get { - return ResourceManager.GetString("DownloadingQualityProfilesNotNeeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to create the configuration for language {0}. - /// - internal static string FailedToCreateBindingConfigForLanguage { - get { - return ResourceManager.GetString("FailedToCreateBindingConfigForLanguage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Number of out of date Quality Profiles: {0}. - /// - internal static string OutOfDateQPsFound { - get { - return ResourceManager.GetString("OutOfDateQPsFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/QualityProfiles] . - /// - internal static string QPMessagePrefix { - get { - return ResourceManager.GetString("QPMessagePrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Successfully downloaded quality profile. Name: '{0}', Key: '{1}', Language: '{2}'. - /// - internal static string QualityProfileDownloadSuccessfulMessageFormat { - get { - return ResourceManager.GetString("QualityProfileDownloadSuccessfulMessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}. - /// - internal static string SubTextPaddingFormat { - get { - return ResourceManager.GetString("SubTextPaddingFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Updating quality profiles.... - /// - internal static string UpdatingQualityProfiles { - get { - return ResourceManager.GetString("UpdatingQualityProfiles", resourceCulture); - } - } - } -} diff --git a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx b/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx deleted file mode 100644 index 8c49109c56..0000000000 --- a/src/ConnectedMode/QualityProfiles/QualityProfilesStrings.resx +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Downloading quality profile: {0} - A progress notification message e.g. "Downloading quality profile: C#" - - - Failed to create the configuration for language {0} - Output window message displayed when the application failed to fetch and create the binding configuration associated with a language. {0} the language name. - - - Successfully downloaded quality profile. Name: '{0}', Key: '{1}', Language: '{2}' - Output window message indicating the quality profile file was successfully downloaded. {0} quality profile name, {1} quality profile key, {2} language name. - - - {0} - Output window message format. Used whenever the message should be displayed as a sub-message from a step message so the user can clearly identify the grouping. {0} the actual message to display. - - - All quality profiles are up to date - - - [ConnectedMode/QualityProfiles] - - - Number of out of date Quality Profiles: {0} - - - Updating quality profiles... - - \ No newline at end of file diff --git a/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs b/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs deleted file mode 100644 index 23477121ce..0000000000 --- a/src/ConnectedMode/QualityProfiles/RoslynQualityProfileDownloader.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using SonarLint.VisualStudio.ConnectedMode.Binding; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode.QualityProfiles -{ - internal interface IQualityProfileDownloader - { - /// - /// Ensures that the Quality Profiles for all supported languages are to date - /// - /// true if there were changes updated, false if everything is up to date - /// If binding failed for one of the languages - Task UpdateAsync(BoundServerProject boundProject, IProgress progress, CancellationToken cancellationToken); - } - - [Export(typeof(IQualityProfileDownloader))] - [PartCreationPolicy(CreationPolicy.Shared)] - [method: ImportingConstructor] - [ExcludeFromCodeCoverage] // todo https://sonarsource.atlassian.net/browse/SLVS-2420 - internal class RoslynQualityProfileDownloader() - : IQualityProfileDownloader - { - public async Task UpdateAsync( - BoundServerProject boundProject, - IProgress progress, - CancellationToken cancellationToken) - { - // TODO by https://sonarsource.atlassian.net/browse/SLVS-2420 drop this class - return false; - } - } -} diff --git a/src/ConnectedMode/ServerQueryInfoProvider.cs b/src/ConnectedMode/ServerQueryInfoProvider.cs deleted file mode 100644 index 8947b8b4a2..0000000000 --- a/src/ConnectedMode/ServerQueryInfoProvider.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - /// - /// Returns the information necessary to make various queries to the Sonar server - /// - /// Facade to simplify classes that need to make branch-aware calls. - /// The projectKey and server branch are provided by different interfaces. This interface - /// simplifies getting the information. - internal interface IServerQueryInfoProvider - { - /// - /// Returns the projectKey and branch name for the current bound solution, or - /// (null, null) if the solution is not bound - /// - Task<(string projectKey, string branchName)> GetProjectKeyAndBranchAsync(CancellationToken token); - } - - [Export(typeof(IServerQueryInfoProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class ServerQueryInfoProvider : IServerQueryInfoProvider - { - private readonly IConfigurationProvider configurationProvider; - private readonly IStatefulServerBranchProvider serverBranchProvider; - - [ImportingConstructor] - public ServerQueryInfoProvider(IConfigurationProvider configurationProvider, IStatefulServerBranchProvider serverBranchProvider) - { - this.configurationProvider = configurationProvider; - this.serverBranchProvider = serverBranchProvider; - } - - public async Task<(string projectKey, string branchName)> GetProjectKeyAndBranchAsync(CancellationToken token) - { - var config = configurationProvider.GetConfiguration(); - if (config.Mode == SonarLintMode.Standalone) - { - return (null, null); - } - - var branchName = await serverBranchProvider.GetServerBranchNameAsync(token); - return (config.Project.ServerProjectKey, branchName); - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs b/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs deleted file mode 100644 index dca9008879..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSource.cs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - internal interface IIssueServerEventSource : IServerSentEventSource { } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs b/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs deleted file mode 100644 index cf0f3c07d9..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IIssueServerEventSourcePublisher.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.SonarQubeClient; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - public interface IIssueServerEventSourcePublisher : IServerSentEventSourcePublisher - { - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs b/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs deleted file mode 100644 index a2c59687c2..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSource.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - /// - /// Source for the server sent events about certain server side changes to the SQ/SC project - /// - /// This interface is not intended to be thread safe - /// Server sent event type inherited from - public interface IServerSentEventSource where T : class, IServerEvent - { - /// - /// Method that is used to await for the next server sent event . - /// - /// Does not throw, always returns null after it's disposed - /// - /// Task which result is: - /// - /// - /// Next server event in queue - /// - /// - /// Or null, when the channel has been Disposed - /// - /// - /// - Task GetNextEventOrNullAsync(); - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs b/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs deleted file mode 100644 index 5203ad7e56..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IServerSentEventSourcePublisher.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.SonarQubeClient -{ - /// - /// The publishing side for the - /// - /// This interface is not intended to be thread safe. - /// The only permitted type of multi threaded calling is calling Publish and Dispose concurrently, although it may result in - /// Server sent event type inherited from - public interface IServerSentEventSourcePublisher : IDisposable where T : class, IServerEvent - { - /// - /// Publishes the event to the consumer channel. - /// After the instance has been disposed. - /// - /// Server event () that needs to be delivered to the consumer - /// - void Publish(T serverEvent); - } -} diff --git a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs b/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs deleted file mode 100644 index 7da371c2be..0000000000 --- a/src/ConnectedMode/ServerSentEvents/Issue/IssueServerEventChannel.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue -{ - [Export(typeof(IIssueServerEventSource))] - [Export(typeof(IIssueServerEventSourcePublisher))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class IssueServerEventChannel : ServerEventChannel, IIssueServerEventSource, IIssueServerEventSourcePublisher { } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs deleted file mode 100644 index d9878eabfa..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSource.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - /// - internal interface IQualityProfileServerEventSource : IServerSentEventSource - { - } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs deleted file mode 100644 index cd011e7dc8..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/IQualityProfileServerEventSourcePublisher.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.ConnectedMode.SonarQubeClient; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - /// - internal interface IQualityProfileServerEventSourcePublisher : IServerSentEventSourcePublisher - { - } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs deleted file mode 100644 index f768e26714..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventChannel.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - [Export(typeof(IQualityProfileServerEventSource))] - [Export(typeof(IQualityProfileServerEventSourcePublisher))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class QualityProfileServerEventChannel : ServerEventChannel, IQualityProfileServerEventSource, IQualityProfileServerEventSourcePublisher { } -} diff --git a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs b/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs deleted file mode 100644 index 46cdf1de5a..0000000000 --- a/src/ConnectedMode/ServerSentEvents/QualityProfile/QualityProfileServerEventsListener.cs +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Threading.Tasks; -using SonarLint.VisualStudio.ConnectedMode.QualityProfiles; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile -{ - /// - /// Event listener for - /// - internal interface IQualityProfileServerEventsListener - { - Task ListenAsync(); - } - - [Export(typeof(IQualityProfileServerEventsListener))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal class QualityProfileServerEventsListener : IQualityProfileServerEventsListener - { - private readonly IQualityProfileServerEventSource eventSource; - private readonly IQualityProfileUpdater updater; - private readonly IThreadHandling threadHandling; - - [ImportingConstructor] - public QualityProfileServerEventsListener(IQualityProfileServerEventSource eventSource, IQualityProfileUpdater updater, IThreadHandling threadHandling) - { - this.eventSource = eventSource; - this.updater = updater; - this.threadHandling = threadHandling; - } - - public async Task ListenAsync() - { - await threadHandling.SwitchToBackgroundThread(); - - // when event source is disposed, it returns null - while (await eventSource.GetNextEventOrNullAsync() != null) - { - // the update is cancelled via it's own Dispose, managed elsewhere - await updater.UpdateAsync(); - } - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs b/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs deleted file mode 100644 index 5558e6c90b..0000000000 --- a/src/ConnectedMode/ServerSentEvents/SSESessionFactory.cs +++ /dev/null @@ -1,212 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.QualityProfile; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client; -using SonarQube.Client.Models.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents; - -internal delegate Task OnSessionFailedAsync(ISSESession failedSession); - -/// -/// Factory for . Responsible for disposing EventSourcePublishers -/// -internal interface ISSESessionFactory : IDisposable -{ - ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback); -} - -/// -/// Represents the session entity, that is responsible for dealing with reader -/// and propagating events to correct topic event publishers -/// -internal interface ISSESession : IDisposable -{ - Task PumpAllAsync(); -} - -[Export(typeof(ISSESessionFactory))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class SSESessionFactory : ISSESessionFactory -{ - private readonly ISonarQubeService sonarQubeClient; - private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; - private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; - private readonly IThreadHandling threadHandling; - - private bool disposed; - private readonly ILogger logger; - - [ImportingConstructor] - public SSESessionFactory(ISonarQubeService sonarQubeClient, - IIssueServerEventSourcePublisher issueServerEventSourcePublisher, - IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, - IThreadHandling threadHandling, - ILogger logger) - { - this.sonarQubeClient = sonarQubeClient; - this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; - this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; - this.threadHandling = threadHandling; - this.logger = logger; - } - - public ISSESession Create(string projectKey, OnSessionFailedAsync onSessionFailedCallback) - { - if (disposed) - { - throw new ObjectDisposedException(nameof(SSESessionFactory)); - } - - var session = new SSESession( - issueServerEventSourcePublisher, - qualityProfileServerEventSourcePublisher, - projectKey, - threadHandling, - sonarQubeClient, - onSessionFailedCallback, - logger); - - return session; - } - - public void Dispose() - { - if (disposed) - { - return; - } - - issueServerEventSourcePublisher.Dispose(); - qualityProfileServerEventSourcePublisher.Dispose(); - disposed = true; - } - - internal sealed class SSESession : ISSESession - { - private readonly IIssueServerEventSourcePublisher issueServerEventSourcePublisher; - private readonly IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher; - private readonly string projectKey; - private readonly IThreadHandling threadHandling; - private readonly ISonarQubeService sonarQubeService; - private readonly OnSessionFailedAsync onSessionFailedCallback; - private readonly ILogger logger; - private readonly CancellationTokenSource sessionTokenSource; - - private bool disposed; - - internal SSESession(IIssueServerEventSourcePublisher issueServerEventSourcePublisher, - IQualityProfileServerEventSourcePublisher qualityProfileServerEventSourcePublisher, - string projectKey, - IThreadHandling threadHandling, - ISonarQubeService sonarQubeService, - OnSessionFailedAsync onSessionFailedCallback, - ILogger logger) - { - this.issueServerEventSourcePublisher = issueServerEventSourcePublisher; - this.qualityProfileServerEventSourcePublisher = qualityProfileServerEventSourcePublisher; - this.projectKey = projectKey; - this.threadHandling = threadHandling; - this.sonarQubeService = sonarQubeService; - this.onSessionFailedCallback = onSessionFailedCallback; - this.logger = logger; - this.sessionTokenSource = new CancellationTokenSource(); - } - - public async Task PumpAllAsync() - { - if (disposed) - { - logger.LogVerbose("[SSESession] Session {0} is disposed", GetHashCode()); - throw new ObjectDisposedException(nameof(SSESession)); - } - - await threadHandling.SwitchToBackgroundThread(); - - var sseStreamReader = await sonarQubeService.CreateSSEStreamReader(projectKey, sessionTokenSource.Token); - - if (sseStreamReader == null) - { - logger.LogVerbose("[SSESession] Failed to create CreateSSEStreamReader"); - return; - } - - while (!sessionTokenSource.IsCancellationRequested) - { - try - { - var serverEvent = await sseStreamReader.ReadAsync(); - - if (serverEvent == null) - { - continue; - } - - logger.LogVerbose("[SSESession] Received server event: {0}", serverEvent.GetType()); - - switch (serverEvent) - { - case IIssueChangedServerEvent issueChangedServerEvent: - { - logger.LogVerbose("[SSESession] Publishing issue changed event..."); - issueServerEventSourcePublisher.Publish(issueChangedServerEvent); - break; - } - case IQualityProfileEvent qualityProfileEvent: - { - logger.LogVerbose("[SSESession] Publishing quality profile event..."); - qualityProfileServerEventSourcePublisher.Publish(qualityProfileEvent); - break; - } - } - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose($"[SSESession] Failed to handle events: {ex}"); - onSessionFailedCallback(this).Forget(); - Dispose(); - return; - } - } - - logger.LogVerbose("[SSESession] Session stopped, session token was canceled"); - } - - public void Dispose() - { - logger.LogVerbose("[SSESession] Disposing session: {0}", GetHashCode()); - - if (disposed) - { - return; - } - - disposed = true; - sessionTokenSource.Cancel(); - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs b/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs deleted file mode 100644 index 8ac771b4cb..0000000000 --- a/src/ConnectedMode/ServerSentEvents/SSESessionManager.cs +++ /dev/null @@ -1,134 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.ConnectedMode.ServerSentEvents -{ - [Export(typeof(SSESessionManager))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class SSESessionManager : IDisposable - { - private const int DelayTimeBetweenRetriesInMilliseconds = 1000; - - private readonly object syncRoot = new object(); - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly ISSESessionFactory sseSessionFactory; - private readonly ILogger logger; - - private ISSESession currentSession; - - private bool disposed; - - [ImportingConstructor] - public SSESessionManager(IActiveSolutionBoundTracker activeSolutionBoundTracker, - ISSESessionFactory sseSessionFactory, - ILogger logger) - { - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.sseSessionFactory = sseSessionFactory; - this.logger = logger; - - activeSolutionBoundTracker.SolutionBindingChanged += SolutionBindingChanged; - } - - public void Dispose() - { - if (disposed) - { - return; - } - - activeSolutionBoundTracker.SolutionBindingChanged -= SolutionBindingChanged; - sseSessionFactory.Dispose(); - EndCurrentSession(); - disposed = true; - } - - private void SolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs activeSolutionBindingEventArgs) - { - CreateSessionIfInConnectedMode(activeSolutionBindingEventArgs.Configuration); - } - - /// - /// Creates a new session if in connected mode. If no binding configuration is provided the - /// ActiveSolutionBoundTracker.CurrentConfiguration will be used. - /// - public void CreateSessionIfInConnectedMode(BindingConfiguration bindingConfiguration = null) - { - if (bindingConfiguration == null) { bindingConfiguration = activeSolutionBoundTracker.CurrentConfiguration; } - - lock (syncRoot) - { - EndCurrentSession(); - - var isInConnectedMode = !bindingConfiguration.Equals(BindingConfiguration.Standalone); - - if (!isInConnectedMode) - { - logger.LogVerbose("[SSESessionManager] Not in connected mode"); - return; - } - - if (bindingConfiguration.Project.ServerConnection is ServerConnection.SonarCloud) - { - logger.LogVerbose("[SSESessionManager] Not available for the current server connection"); - return; - } - - logger.LogVerbose("[SSESessionManager] In connected mode, creating session..."); - - currentSession = sseSessionFactory.Create(bindingConfiguration.Project.ServerProjectKey, OnSessionFailedAsync); - - logger.LogVerbose("[SSESessionManager] Created session: {0}", currentSession.GetHashCode()); - - currentSession.PumpAllAsync().Forget(); - - logger.LogVerbose("[SSESessionManager] Session started"); - } - } - - private async Task OnSessionFailedAsync(ISSESession failedSession) - { - logger.LogVerbose("[SSESessionManager] Session failed: " + failedSession.GetHashCode()); - - await Task.Delay(DelayTimeBetweenRetriesInMilliseconds); - CreateSessionIfInConnectedMode(activeSolutionBoundTracker.CurrentConfiguration); - - logger.LogVerbose("[SSESessionManager] Finished handling session failure"); - } - - private void EndCurrentSession() - { - logger.LogVerbose("[SSESessionManager] Ending current session..."); - - lock (syncRoot) - { - logger.LogVerbose("[SSESessionManager] Disposing current session: {0}", currentSession?.GetHashCode()); - - currentSession?.Dispose(); - currentSession = null; - } - } - } -} diff --git a/src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs b/src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs deleted file mode 100644 index 841b05aaf9..0000000000 --- a/src/ConnectedMode/ServerSentEvents/ServerEventChannel.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Threading.Channels; -using SonarLint.VisualStudio.ConnectedMode.ServerSentEvents.Issue; -using SonarLint.VisualStudio.ConnectedMode.SonarQubeClient; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.Core.ServerSentEvents -{ - /// - /// Base class for channels of server events divided by topics - /// - /// - /// Even though and do not allow their own methods to be called from different threads, this class was designed to allow the calls to two different interfaces' methods to be made from different threads at the same time. - /// This means that calling from one thread and calling or from a different thread at the same time is allowed, while calling methods of the same interface from different threads is not. - /// NOTE: there's an exception to this rule that was made to simplify the implementation of the events pump that permits calling Dispose and Publish concurrently, but results in an exception. For more info see - /// - /// - public class ServerEventChannel : IServerSentEventSource, IServerSentEventSourcePublisher where T : class, IServerEvent - { - private bool disposed; - /// - /// Channel that is used for storing and awaiting new items - /// - private readonly Channel channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true }); - - public async Task GetNextEventOrNullAsync() - { - await channel.Reader.WaitToReadAsync().ConfigureAwait(false); - return channel.Reader.TryRead(out var item) - ? item - : null; - } - - public void Publish(T serverEvent) - { - // The behaviour for TryWrite depends, in some aspects, on the type of the channel we use.In particular, what we are interested in is what it returns before and after marking the channel as complete. - // Because we specifically create an unbound channel with the SingleReader = true, SingleWriter = true settings, we actually get a SingleConsumerUnboundedChannel instance that has the following behaviour: TryWrite always returns true before the channel is marked as complete, and always returns false afterwards. - // This behaviour allows us to be sure that the only way we can interpret the false result is that the channel was closed. - // This is valuable for us because the only other way of knowing that would be calling WriteAsync and catching the exception, as the ChannelReader.Completed task is only resolved when there's no more items in the channel, and that may not happen at the same time as we called ChannelWriter.Complete. - // So in short, the reason for this assumption was to simplify our channel wrapper code. - if (!channel.Writer.TryWrite(serverEvent ?? throw new ArgumentNullException(nameof(serverEvent)))) - { - throw new ObjectDisposedException(nameof(ServerEventChannel)); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - channel.Writer.Complete(); - disposed = true; - } - } -} diff --git a/src/Core.UnitTests/Binding/BoundServerProjectTests.cs b/src/Core.UnitTests/Binding/BoundServerProjectTests.cs index 84ace35b94..2f8ccf322e 100644 --- a/src/Core.UnitTests/Binding/BoundServerProjectTests.cs +++ b/src/Core.UnitTests/Binding/BoundServerProjectTests.cs @@ -66,6 +66,5 @@ public void Ctor_SetsValues() boundServerProject.LocalBindingKey.Should().BeSameAs(localBindingKey); boundServerProject.ServerProjectKey.Should().BeSameAs(serverProjectKey); boundServerProject.ServerConnection.Should().BeSameAs(serverConnection); - boundServerProject.Profiles.Should().BeNull(); } } diff --git a/src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs b/src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs deleted file mode 100644 index 6a555982de..0000000000 --- a/src/Core.UnitTests/ServerSentEvents/ServerEventChannelTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarLint.VisualStudio.Core.UnitTests.ServerSentEvents -{ - [TestClass] - public class ServerEventChannelTests - { - [TestMethod] - public async Task Get_AfterPublish_ReturnsCorrectValue() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - testSubject.Publish(serverEvent); - var receivedEvent = await testSubject.GetNextEventOrNullAsync(); - - receivedEvent.Should().BeSameAs(serverEvent); - } - - [TestMethod] - public async Task Get_AwaitsForPublishBeforeReturning() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - var getTask = testSubject.GetNextEventOrNullAsync(); - testSubject.Publish(serverEvent); - var receivedEvent = await getTask; - - receivedEvent.Should().BeSameAs(serverEvent); - } - - [TestMethod] - public async Task Get_ReturnsMultiplePublishedItemsInCorrectOrder() - { - var testSubject = CreateTestSubject(); - var serverEvents = Enumerable.Range(0, 5).Select(_ => Mock.Of()).ToList(); - var receivedEvents = new List(); - - foreach (var serverEvent in serverEvents) - { - testSubject.Publish(serverEvent); - } - for (var i = 0; i < serverEvents.Count; i++) - { - receivedEvents.Add(await testSubject.GetNextEventOrNullAsync()); - } - - receivedEvents.Should().BeEquivalentTo(serverEvents); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task Get_AlreadyDisposedAndNoMoreItems_ReturnsNull(bool isEmptyChannel) - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - if (!isEmptyChannel) - { - testSubject.Publish(serverEvent); - } - testSubject.Dispose(); - var receivedEvent = await testSubject.GetNextEventOrNullAsync(); - var receivedEvent2 = await testSubject.GetNextEventOrNullAsync(); - - receivedEvent.Should().BeSameAs(isEmptyChannel ? null : serverEvent); - receivedEvent2.Should().BeNull(); - } - - [TestMethod] - public async Task Get_AfterDispose_ReturnsNull() - { - var testSubject = CreateTestSubject(); - - testSubject.Dispose(); - var receivedEvent = await testSubject.GetNextEventOrNullAsync(); - - receivedEvent.Should().BeNull(); - } - - [TestMethod] - public async Task Get_DisposedWhileAwaiting_ReturnsNull() - { - var testSubject = CreateTestSubject(); - - var getTask = testSubject.GetNextEventOrNullAsync(); - testSubject.Dispose(); - var receivedEvent = await getTask; - - receivedEvent.Should().BeNull(); - } - - [TestMethod] - public void Publish_AlreadyDisposed_ThrowsObjectDisposedException() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - testSubject.Publish(serverEvent); - testSubject.Dispose(); - Assert.ThrowsException(() => testSubject.Publish(serverEvent)); - } - - [TestMethod] - public void Publish_WhenEventIsNull_ThrowsArgumentNullException() - { - var testSubject = CreateTestSubject(); - - Assert.ThrowsException(() => testSubject.Publish(null)); - } - - [TestMethod] - public void Dispose_IsIdempotent() - { - var testSubject = CreateTestSubject(); - var serverEvent = Mock.Of(); - - testSubject.Dispose(); - testSubject.Dispose(); - - Assert.ThrowsException(() => testSubject.Publish(serverEvent)); - } - - private ServerEventChannel CreateTestSubject() - { - return new ServerEventChannel(); - } - } -} diff --git a/src/Core/Binding/BoundServerProject.cs b/src/Core/Binding/BoundServerProject.cs index a5a6a53009..2969ce41af 100644 --- a/src/Core/Binding/BoundServerProject.cs +++ b/src/Core/Binding/BoundServerProject.cs @@ -25,7 +25,6 @@ public class BoundServerProject public string LocalBindingKey { get; } public string ServerProjectKey { get; } public ServerConnection ServerConnection { get; } - public Dictionary Profiles { get; set; } public BoundServerProject(string localBindingKey, string serverProjectKey, ServerConnection serverConnection) { diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs deleted file mode 100644 index 269f83911c..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerArrayComparerTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class AnalyzerArrayComparerTests -{ - [TestMethod] - public void Equals_BothNulls_True() - { - AnalyzerArrayComparer.Instance.Equals(null, null).Should().BeTrue(); - } - - [TestMethod] - public void Equals_OneNull_False() - { - var analyzerArray = ImmutableArray.Create(); - AnalyzerArrayComparer.Instance.Equals(null, analyzerArray).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(analyzerArray, null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_EmptyArrays_True() - { - var analyzerArray1 = ImmutableArray.Create(); - var analyzerArray2 = ImmutableArray.Create(); - AnalyzerArrayComparer.Instance.Equals(analyzerArray1, analyzerArray2).Should().BeTrue(); - AnalyzerArrayComparer.Instance.Equals(analyzerArray2, analyzerArray1).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DifferentLengths_False() - { - var empty = ImmutableArray.Create(); - var single = ImmutableArray.Create(GetAnalyzerFileReference()); - var triple = ImmutableArray.Create(GetAnalyzerFileReference(), GetAnalyzerFileReference(), GetAnalyzerFileReference()); - AnalyzerArrayComparer.Instance.Equals(empty, single).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(single, empty).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(single, triple).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(triple, single).Should().BeFalse(); - } - - [TestMethod] - public void Equals_SameArray_True() - { - var array = ImmutableArray.Create(GetAnalyzerFileReference()); - AnalyzerArrayComparer.Instance.Equals(array, array).Should().BeTrue(); - } - - [TestMethod] - public void Equals_ArrayWithSameValuesInOrder_True() - { - var analyzerFileReference1 = GetAnalyzerFileReference(); - var analyzerFileReference2 = GetAnalyzerFileReference(); - var analyzerFileReference3 = GetAnalyzerFileReference(); - var array1 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference2, analyzerFileReference3); - var array2 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference2, analyzerFileReference3); - AnalyzerArrayComparer.Instance.Equals(array1, array2).Should().BeTrue(); - AnalyzerArrayComparer.Instance.Equals(array2, array1).Should().BeTrue(); - } - - [TestMethod] - public void Equals_ArrayWithSameValuesInDifferentOrder_False() - { - var analyzerFileReference1 = GetAnalyzerFileReference(); - var analyzerFileReference2 = GetAnalyzerFileReference(); - var analyzerFileReference3 = GetAnalyzerFileReference(); - var array1 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference3, analyzerFileReference2); - var array2 = ImmutableArray.Create(analyzerFileReference1, analyzerFileReference2, analyzerFileReference3); - AnalyzerArrayComparer.Instance.Equals(array1, array2).Should().BeFalse(); - AnalyzerArrayComparer.Instance.Equals(array2, array1).Should().BeFalse(); - } - - [TestMethod] - public void Equals_ArrayWithEquivalentValues_True() - { - var analyzerAssemblyLoader1 = Substitute.For(); - var analyzerAssemblyLoader2 = Substitute.For(); - var analyzerFileReference11 = GetAnalyzerFileReference(@"C:\analyzer1", analyzerAssemblyLoader1); - var analyzerFileReference12 = GetAnalyzerFileReference(@"C:\analyzer1", analyzerAssemblyLoader1); - var analyzerFileReference21 = GetAnalyzerFileReference(@"C:\analyzer2", analyzerAssemblyLoader2); - var analyzerFileReference22 = GetAnalyzerFileReference(@"C:\analyzer2", analyzerAssemblyLoader2); - var array1 = ImmutableArray.Create(analyzerFileReference11, analyzerFileReference21); - var array2 = ImmutableArray.Create(analyzerFileReference12, analyzerFileReference22); - AnalyzerArrayComparer.Instance.Equals(array1, array2).Should().BeTrue(); - AnalyzerArrayComparer.Instance.Equals(array2, array1).Should().BeTrue(); - } - - [TestMethod] - public void GetHashCode_DelegatesToObject() - { - ImmutableArray? nullArray = null; - AnalyzerArrayComparer.Instance.GetHashCode(nullArray).Should().Be(nullArray.GetHashCode()); - var analyzerFileReferences = ImmutableArray.Create(); - AnalyzerArrayComparer.Instance.GetHashCode(analyzerFileReferences).Should().Be(analyzerFileReferences.GetHashCode()); - } - - private AnalyzerFileReference GetAnalyzerFileReference(string filePath = @"C:\analyzer", IAnalyzerAssemblyLoader analyzerAssemblyLoader = null) - { - return new AnalyzerFileReference(filePath, analyzerAssemblyLoader ?? Substitute.For()); - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs deleted file mode 100644 index 5626430707..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerAssemblyLoaderFactoryTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class AnalyzerAssemblyLoaderFactoryTests -{ - private AnalyzerAssemblyLoaderFactory testSubject; - - [TestInitialize] - public void TestInitialize() - { - testSubject = new AnalyzerAssemblyLoaderFactory(); - } - - [TestMethod] - public void MefCtor_CheckExports() - { - MefTestHelpers.CheckTypeCanBeImported(); - } - - [TestMethod] - public void Mef_CheckIsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Create_CachesLoader() - { - var loader1 = testSubject.Create(); - var loader2 = testSubject.Create(); - - Assert.AreSame(loader1, loader2); - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs deleted file mode 100644 index 8b2eb5db22..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/AnalyzerChangeTests.cs +++ /dev/null @@ -1,192 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class AnalyzerChangeTests -{ - private IRoslynSolutionWrapper solution; - private AnalyzerFileReference analyzer1; - private AnalyzerFileReference analyzer2; - private AnalyzerFileReference analyzer3; - private AnalyzerFileReference analyzer4; - - [TestInitialize] - public void TestInitialize() - { - solution = Substitute.For(); - - analyzer1 = new AnalyzerFileReference(@"C:\abc1", Substitute.For()); - analyzer2 = new AnalyzerFileReference(@"C:\abc2", Substitute.For()); - analyzer3 = new AnalyzerFileReference(@"C:\abc3", Substitute.For()); - analyzer4 = new AnalyzerFileReference(@"C:\abc4", Substitute.For()); - } - - [TestMethod] - public void Properties_SetToCorrectValues() - { - var analyzersToRemove = ImmutableArray.Create(analyzer1, analyzer2); - var analyzersToAdd = ImmutableArray.Create(analyzer3, analyzer4); - var testSubject = new AnalyzerChange(analyzersToRemove, analyzersToAdd); - - testSubject.AnalyzersToRemove.Should().BeEquivalentTo(analyzersToRemove); - testSubject.AnalyzersToAdd.Should().BeEquivalentTo(analyzersToAdd); - } - - [TestMethod] - public void Change_PerformsUpdatesOnSolution() - { - var intermediateSolution = Substitute.For(); - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1, analyzer2); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer3, analyzer4); - SetUpRemove(allAnalyzersToRemove, allAnalyzersToRemove, solution, intermediateSolution); - SetUpAdd(allAnalyzersToAdd, allAnalyzersToAdd, intermediateSolution, resultingSolution); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - } - - [TestMethod] - public void Change_FiltersOutAlreadyContainedOrRemovedAnalyzers() - { - var intermediateSolution = Substitute.For(); - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1, analyzer2); - List actualAnalyzersToRemove = [analyzer1]; - var allAnalyzersToAdd = ImmutableArray.Create(analyzer3, analyzer4); - List actualAnalyzersToAdd = [analyzer4]; - SetUpRemove(allAnalyzersToRemove, actualAnalyzersToRemove, solution, intermediateSolution); - SetUpAdd(allAnalyzersToAdd, actualAnalyzersToAdd, intermediateSolution, resultingSolution); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - Received.InOrder(() => - { - solution.ContainsAnalyzer(analyzer1); - solution.ContainsAnalyzer(analyzer2); - solution.RemoveAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToRemove))); - intermediateSolution.ContainsAnalyzer(analyzer3); - intermediateSolution.ContainsAnalyzer(analyzer4); - intermediateSolution.AddAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToAdd))); - }); - } - - [TestMethod] - public void Change_RemovedAnalyzersAlreadyRemoved_SkipsRemoval() - { - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1); - SetUpRemove(allAnalyzersToRemove, [], solution, default); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer2); - List actualAnalyzersToAdd = [analyzer2]; - SetUpAdd(allAnalyzersToAdd, actualAnalyzersToAdd, solution, resultingSolution); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, - allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().RemoveAnalyzerReferences(default); - } - - - [TestMethod] - public void Change_NothingToRemove_SkipsRemoval() - { - var resultingSolution = Substitute.For(); - SetUpRemove(ImmutableArray.Empty, [], solution, default); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer2); - List actualAnalyzersToAdd = [analyzer2]; - SetUpAdd(allAnalyzersToAdd, actualAnalyzersToAdd, solution, resultingSolution); - var testSubject = new AnalyzerChange(ImmutableArray.Empty, allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().RemoveAnalyzerReferences(default); - } - - [TestMethod] - public void Change_AddedAnalyzersAlreadyAdded_SkipsAddition() - { - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1); - SetUpRemove(allAnalyzersToRemove, [analyzer1], solution, resultingSolution); - var allAnalyzersToAdd = ImmutableArray.Create(analyzer2); - SetUpAdd(allAnalyzersToAdd, [], resultingSolution, default); - - var testSubject = new AnalyzerChange(allAnalyzersToRemove, - allAnalyzersToAdd); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().AddAnalyzerReferences(default); - } - - - [TestMethod] - public void Change_NothingToAdd_SkipsAddition() - { - var resultingSolution = Substitute.For(); - var allAnalyzersToRemove = ImmutableArray.Create(analyzer1); - SetUpRemove(allAnalyzersToRemove, [analyzer1], solution, resultingSolution); - SetUpAdd(ImmutableArray.Empty, [], solution, default); - var testSubject = new AnalyzerChange(allAnalyzersToRemove, - ImmutableArray.Empty); - - testSubject.Change(solution).Should().BeSameAs(resultingSolution); - - solution.DidNotReceiveWithAnyArgs().AddAnalyzerReferences(default); - } - - private static void SetUpAdd( - ImmutableArray allAnalyzersToAdd, - ICollection actualAnalyzersToAdd, - IRoslynSolutionWrapper original, - IRoslynSolutionWrapper resulting) - { - SetUpContains(allAnalyzersToAdd, allAnalyzersToAdd.Except(actualAnalyzersToAdd).ToList(), original); - original.AddAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToAdd))).Returns(resulting); - } - - private static void SetUpRemove( - ImmutableArray allAnalyzersToRemove, - ICollection actualAnalyzersToRemove, - IRoslynSolutionWrapper original, - IRoslynSolutionWrapper resulting) - { - SetUpContains(allAnalyzersToRemove, actualAnalyzersToRemove, original); - original.RemoveAnalyzerReferences(Arg.Is>(x => x.SequenceEqual(actualAnalyzersToRemove))).Returns(resulting); - } - - private static void SetUpContains(IEnumerable allAnalyzers, ICollection containedAnalyzers, IRoslynSolutionWrapper solutionWrapper) - { - foreach (var analyzer in allAnalyzers) - { - solutionWrapper.ContainsAnalyzer(analyzer).Returns(containedAnalyzers.Contains(analyzer)); - } - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs deleted file mode 100644 index e9e58de46b..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/EmbeddedDotnetAnalyzerProviderTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.IO; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class EmbeddedDotnetAnalyzerProviderTests -{ - private const string AnalyzersPath = "C:\\somepath"; - private readonly IAnalyzerAssemblyLoader analyzerAssemblyLoader = Substitute.For(); - private EmbeddedDotnetAnalyzerProvider testSubject; - private IObsoleteDotnetAnalyzersLocator locator; - private IAnalyzerAssemblyLoaderFactory loaderFactory; - private IConfigurationScopeDotnetAnalyzerIndicator indicator; - private ILogger logger; - private IThreadHandling threadHandling; - - [TestInitialize] - public void TestInitialize() - { - locator = Substitute.For(); - loaderFactory = Substitute.For(); - loaderFactory.Create().Returns(analyzerAssemblyLoader); - logger = Substitute.For(); - indicator = Substitute.For(); - threadHandling = new NoOpThreadHandler(); - - testSubject = new EmbeddedDotnetAnalyzerProvider(locator, loaderFactory, indicator, logger, threadHandling); - MockServices(); - } - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void MefCtor_IsSingleton() - { - MefTestHelpers.CheckIsSingletonMefComponent(); - } - - [TestMethod] - public void Ctor_CreatesLoader() - { - var factory = Substitute.For(); - - new EmbeddedDotnetAnalyzerProvider(default, factory, default, default, default); - - factory.Received(1).Create(); - } - - [TestMethod] - public void GetBasicAsync_GetsAnalyzersFromExpectedLocation() - { - testSubject.GetBasicAsync(); - - locator.Received(1).GetBasicAnalyzerFullPaths(); - } - - [TestMethod] - public void GetBasicAsync_RunsOnBackgroundThread() - { - var threadHandlingMock = Substitute.For(); - var subject = new EmbeddedDotnetAnalyzerProvider(default, - Substitute.For(), - default, - default, - threadHandlingMock); - - subject.GetBasicAsync(); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any>>>()); - } - - [TestMethod] - public void GetEnterpriseOrNullAsync_GetsAnalyzersFromExpectedLocation() - { - testSubject.GetEnterpriseOrNullAsync("scope"); - - locator.Received(1).GetEnterpriseAnalyzerFullPaths(); - } - - [TestMethod] - public void GetEnterpriseOrNullAsync_RunsOnBackgroundThread() - { - var threadHandlingMock = Substitute.For(); - var subject = new EmbeddedDotnetAnalyzerProvider(default, - Substitute.For(), - default, - default, - threadHandlingMock); - - subject.GetEnterpriseOrNullAsync("scope"); - - threadHandlingMock.Received(1).RunOnBackgroundThread(Arg.Any?>>>()); - } - - [TestMethod] - public async Task GetBasicAsync_AnalyzerFilesExist_ReturnsAnalyzerFileReference() - { - locator.GetBasicAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll"), GetAnalyzerFullPath("analyzer2.dll")]); - - var analyzerFileReferences = await testSubject.GetBasicAsync(); - - analyzerFileReferences.Should().NotBeNull(); - analyzerFileReferences.Length.Should().Be(2); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences, GetAnalyzerFullPath("analyzer1.dll")); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences, GetAnalyzerFullPath("analyzer2.dll")); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_AnalyzerFilesExist_ReturnsAnalyzerFileReference() - { - const string configurationScopeId = "scope"; - locator.GetEnterpriseAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll"), GetAnalyzerFullPath("analyzer2.dll")]); - indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(configurationScopeId).Returns(true); - - var analyzerFileReferences = await testSubject.GetEnterpriseOrNullAsync(configurationScopeId); - - analyzerFileReferences.Should().NotBeNull(); - analyzerFileReferences!.Value.Length.Should().Be(2); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences.Value, GetAnalyzerFullPath("analyzer1.dll")); - ContainsExpectedAnalyzerFileReference(analyzerFileReferences.Value, GetAnalyzerFullPath("analyzer2.dll")); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_AnalyzerFilesExist_NotEnabledForConfigScope_ReturnsAnalyzerFileReference() - { - const string configurationScopeId = "scope"; - locator.GetEnterpriseAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll"), GetAnalyzerFullPath("analyzer2.dll")]); - indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(configurationScopeId).Returns(false); - - var analyzerFileReferences = await testSubject.GetEnterpriseOrNullAsync(configurationScopeId); - - analyzerFileReferences.Should().BeNull(); - } - - [TestMethod] - public async Task GetBasicAsync_AnalyzerFilesDoNotExist_ReturnsLogsAndThrows() - { - locator.GetBasicAnalyzerFullPaths().Returns([]); - - Func act = () => testSubject.GetBasicAsync(); - - await act.Should().ThrowAsync().WithMessage(Resources.EmbeddedRoslynAnalyzersNotFound); - logger.Received(1).LogVerbose(Resources.EmbeddedRoslynAnalyzersNotFound); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_AnalyzerFilesDoNotExist_ReturnsLogsAndThrows() - { - locator.GetEnterpriseAnalyzerFullPaths().Returns([]); - - Func act = () => testSubject.GetEnterpriseOrNullAsync("scope"); - - await act.Should().ThrowAsync().WithMessage(Resources.EmbeddedRoslynAnalyzersNotFound); - logger.Received(1).LogVerbose(Resources.EmbeddedRoslynAnalyzersNotFound); - } - - [TestMethod] - public async Task GetBasicAsync_CachesAnalyzerFileReferences() - { - await testSubject.GetBasicAsync(); - await testSubject.GetBasicAsync(); - - loaderFactory.Received(1).Create(); - locator.Received(1).GetBasicAnalyzerFullPaths(); - } - - [TestMethod] - public async Task GetEnterpriseOrNullAsync_CachesAnalyzerFileReferences() - { - await testSubject.GetEnterpriseOrNullAsync("scope"); - await testSubject.GetEnterpriseOrNullAsync("other scope"); - - loaderFactory.Received(1).Create(); - locator.Received(1).GetEnterpriseAnalyzerFullPaths(); - } - - private static string GetAnalyzerFullPath(string analyzerFile) - { - return Path.Combine(AnalyzersPath, analyzerFile); - } - - private static void ContainsExpectedAnalyzerFileReference( - ImmutableArray analyzerFileReference, - string analyzerPath) - { - analyzerFileReference.Should().Contain(analyzerFile => analyzerFile.FullPath == analyzerPath); - } - - private void MockServices() - { - indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(default).ReturnsForAnyArgs(true); - locator.GetBasicAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll")]); - locator.GetEnterpriseAnalyzerFullPaths().Returns([GetAnalyzerFullPath("analyzer1.dll")]); - loaderFactory.Create().Returns(analyzerAssemblyLoader); - } -} diff --git a/src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs b/src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs deleted file mode 100644 index 9f289b9e2e..0000000000 --- a/src/Infrastructure.VS.UnitTests/Roslyn/RoslynWorkspaceWrapperTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests.Roslyn; - -[TestClass] -public class RoslynWorkspaceWrapperTests -{ - private IThreadHandling threadHandling; - private AdhocWorkspace currentWorkspace; - private IAnalyzerChange analyzerChange; - private RoslynWorkspaceWrapper testSubject; - - [TestInitialize] - public void TestInitialize() - { - threadHandling = Substitute.For(); - threadHandling.RunOnUIThreadAsync(Arg.Any()).ReturnsForAnyArgs(info => - { - (info[0] as Action)?.Invoke(); - return Task.CompletedTask; - }); - currentWorkspace = new AdhocWorkspace(); - var slnInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, null, []); - currentWorkspace.AddSolution(slnInfo); - analyzerChange = Substitute.For(); - testSubject = new RoslynWorkspaceWrapper(currentWorkspace, threadHandling); - } - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public async Task TryApplyChangesAsync_Success_UpdatesWorkspace() - { - var originalSolution = currentWorkspace.CurrentSolution; - var updatedSolutionWrapper = Substitute.For(); - var solutionThatSuccessfullyUpdates = GetSolutionThatSuccessfullyUpdates(originalSolution); - updatedSolutionWrapper.RoslynSolution.Returns(solutionThatSuccessfullyUpdates); - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns(updatedSolutionWrapper); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().NotBeNull(); - result.RoslynSolution.Should().NotBe(originalSolution); - result.RoslynSolution.Should().BeSameAs(currentWorkspace.CurrentSolution); - AssertCurrentSolutionIsEquivalentTo(updatedSolutionWrapper); - CheckRunChangeOnUIThread(); - } - - [TestMethod] - public async Task TryApplyChangesAsync_NoUpdate_ReturnsOriginal() - { - var originalSolution = currentWorkspace.CurrentSolution; - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns(info => info[0] as IRoslynSolutionWrapper); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().NotBeNull(); - result.RoslynSolution.Should().BeSameAs(originalSolution); - testSubject.CurrentSolution.RoslynSolution.Should().BeSameAs(originalSolution); - CheckRunChangeOnUIThread(); - } - - [TestMethod] - public async Task TryApplyChangesAsync_UpdateFails_RetriesAndFails() - { - var originalSolution = currentWorkspace.CurrentSolution; - var failed = new RoslynSolutionWrapper(GetSolutionThatFailsUpdate()); - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns(failed); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().BeNull(); - testSubject.CurrentSolution.RoslynSolution.Should().BeSameAs(originalSolution); - - Received.InOrder(() => - { - for (var i = 0; i < 5; i++) - { - threadHandling.RunOnUIThreadAsync(Arg.Any()); - analyzerChange.Change(Arg.Any()); - } - }); - } - - [DataRow(1)] - [DataRow(2)] - [DataRow(3)] - [DataRow(4)] - [DataTestMethod] - public async Task TryApplyChangesAsync(int failedUpdateTimes) - { - var originalSolution = currentWorkspace.CurrentSolution; - var failed = new RoslynSolutionWrapper(GetSolutionThatFailsUpdate()); - var success = new RoslynSolutionWrapper(GetSolutionThatSuccessfullyUpdates(originalSolution)); - analyzerChange.Change(Arg.Is(x => x.RoslynSolution == originalSolution)).Returns( - failed, - Enumerable.Repeat(failed, failedUpdateTimes - 1).Append(success).ToArray()); - - var result = await testSubject.TryApplyChangesAsync(analyzerChange); - - result.Should().NotBeNull(); - result.RoslynSolution.Should().NotBe(originalSolution); - result.RoslynSolution.Should().BeSameAs(currentWorkspace.CurrentSolution); - AssertCurrentSolutionIsEquivalentTo(success); - - Received.InOrder(() => - { - for (var i = 0; i < failedUpdateTimes + 1; i++) - { - threadHandling.RunOnUIThreadAsync(Arg.Any()); - analyzerChange.Change(Arg.Any()); - } - }); - } - - [TestMethod] - public void CurrentSolution_WorkspaceUpdated_ReturnsUpToDateValue() - { - var originalSolution = currentWorkspace.CurrentSolution; - var solutionThatSuccessfullyUpdates = GetSolutionThatSuccessfullyUpdates(originalSolution); - currentWorkspace.TryApplyChanges(solutionThatSuccessfullyUpdates).Should().BeTrue(); - var updatedSolution = currentWorkspace.CurrentSolution; - - var result = testSubject.CurrentSolution.RoslynSolution; - - result.Should().NotBeSameAs(originalSolution); - result.Should().BeSameAs(updatedSolution); - } - - private void CheckRunChangeOnUIThread() => - Received.InOrder(() => - { - threadHandling.RunOnUIThreadAsync(Arg.Any()); - analyzerChange.Change(Arg.Any()); - }); - - private static Solution GetSolutionThatFailsUpdate() => new AdhocWorkspace().AddSolution(SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, null, [])); - - private static Solution GetSolutionThatSuccessfullyUpdates(Solution originalSolution) - { - var analyzerAssemblyLoader = Substitute.For(); - var analyzerFileReference = new AnalyzerFileReference(@"C:\some\file", analyzerAssemblyLoader); - var updatedSolution = originalSolution.AddAnalyzerReference(analyzerFileReference); - return updatedSolution; - } - - private void AssertCurrentSolutionIsEquivalentTo(IRoslynSolutionWrapper solution) => - solution.Should().NotBeNull().And.BeAssignableTo().Subject.RoslynSolution.AnalyzerReferences.Should() - .BeEquivalentTo(testSubject.CurrentSolution.RoslynSolution.AnalyzerReferences); -} diff --git a/src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs b/src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs deleted file mode 100644 index d29069174b..0000000000 --- a/src/Infrastructure.VS/Roslyn/AnalyzerArrayComparer.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -internal class AnalyzerArrayComparer : IEqualityComparer?> -{ - public static AnalyzerArrayComparer Instance { get; } = new(); - - private AnalyzerArrayComparer() - { - } - - public bool Equals(ImmutableArray? x, ImmutableArray? y) - { - if (x is null && y is null) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - return x.Value.SequenceEqual(y.Value); - } - - public int GetHashCode(ImmutableArray? obj) => obj.GetHashCode(); -} diff --git a/src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs b/src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs deleted file mode 100644 index 9e46653107..0000000000 --- a/src/Infrastructure.VS/Roslyn/AnalyzerAssemblyLoaderFactory.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Microsoft.CodeAnalysis; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -[Export(typeof(IAnalyzerAssemblyLoaderFactory))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal class AnalyzerAssemblyLoaderFactory : IAnalyzerAssemblyLoaderFactory -{ - private AnalyzerAssemblyLoader analyzerAssemblyLoader; - - [ImportingConstructor] - public AnalyzerAssemblyLoaderFactory() - { - } - - public IAnalyzerAssemblyLoader Create() - { - analyzerAssemblyLoader ??= new AnalyzerAssemblyLoader(); - return analyzerAssemblyLoader; - } - - [ExcludeFromCodeCoverage] - private sealed class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader - { - public void AddDependencyLocation(string fullPath) - { - } - - public Assembly LoadFromPath(string fullPath) - { - return Assembly.LoadFrom(fullPath); - } - } -} diff --git a/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs b/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs deleted file mode 100644 index e61f09c79a..0000000000 --- a/src/Infrastructure.VS/Roslyn/EmbeddedDotnetAnalyzerProvider.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.CSharpVB; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -[Export(typeof(IBasicRoslynAnalyzerProvider))] -[Export(typeof(IEnterpriseRoslynAnalyzerProvider))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -public class EmbeddedDotnetAnalyzerProvider( - IObsoleteDotnetAnalyzersLocator locator, - IAnalyzerAssemblyLoaderFactory analyzerAssemblyLoaderFactory, - IConfigurationScopeDotnetAnalyzerIndicator indicator, - ILogger logger, - IThreadHandling threadHandling) - : IBasicRoslynAnalyzerProvider, IEnterpriseRoslynAnalyzerProvider -{ - private readonly IAnalyzerAssemblyLoader loader = analyzerAssemblyLoaderFactory.Create(); - private ImmutableArray? basicAnalyzers; - private ImmutableArray? enterpriseAnalyzers; - - public Task> GetBasicAsync() => - threadHandling.RunOnBackgroundThread(() => - { - basicAnalyzers ??= CreateAnalyzerFileReferences(locator.GetBasicAnalyzerFullPaths()); - - return Task.FromResult(basicAnalyzers.Value); - }); - - public Task?> GetEnterpriseOrNullAsync(string configurationScopeId) => - threadHandling.RunOnBackgroundThread(async () => - { - if (!await indicator.ShouldUseEnterpriseCSharpAnalyzerAsync(configurationScopeId)) - { - return null; - } - - enterpriseAnalyzers ??= CreateAnalyzerFileReferences(locator.GetEnterpriseAnalyzerFullPaths()); - return enterpriseAnalyzers; - - }); - - private ImmutableArray CreateAnalyzerFileReferences(List analyzerPaths) - { - if (analyzerPaths.Count == 0) - { - logger.LogVerbose(Resources.EmbeddedRoslynAnalyzersNotFound); - throw new InvalidOperationException(Resources.EmbeddedRoslynAnalyzersNotFound); - } - - return analyzerPaths.Select(path => new AnalyzerFileReference(path, loader)).ToImmutableArray(); - } -} diff --git a/src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs b/src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs deleted file mode 100644 index e65b9396aa..0000000000 --- a/src/Infrastructure.VS/Roslyn/IAnalyzerAssemblyLoaderFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -public interface IAnalyzerAssemblyLoaderFactory -{ - IAnalyzerAssemblyLoader Create(); -} diff --git a/src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs b/src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs deleted file mode 100644 index 515ed39f52..0000000000 --- a/src/Infrastructure.VS/Roslyn/IBasicRoslynAnalyzerProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -public interface IBasicRoslynAnalyzerProvider -{ - /// - /// Returns SonarAnalyzer.CSharp & SonarAnalyzer.VisualBasic analyzer DLLs that are embedded in the VSIX. - /// If no analyzer is found, throws an exception - /// - Task> GetBasicAsync(); -} diff --git a/src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs b/src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs deleted file mode 100644 index 2b430e46a2..0000000000 --- a/src/Infrastructure.VS/Roslyn/IEnterpriseRoslynAnalyzerProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core.Binding; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -public interface IEnterpriseRoslynAnalyzerProvider -{ - /// - /// Returns SonarAnalyzer.CSharp & SonarAnalyzer.VisualBasic analyzer DLLs that are downloaded from the server for the current binding - /// - Task?> GetEnterpriseOrNullAsync(string configurationScopeId); -} diff --git a/src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs b/src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs deleted file mode 100644 index 69e1eb61cf..0000000000 --- a/src/Infrastructure.VS/Roslyn/IObsoleteDotnetAnalyzersLocator.cs +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -public interface IObsoleteDotnetAnalyzersLocator -{ - List GetBasicAnalyzerFullPaths(); - List GetEnterpriseAnalyzerFullPaths(); -} diff --git a/src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs b/src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs deleted file mode 100644 index d665c92cd2..0000000000 --- a/src/Infrastructure.VS/Roslyn/IRoslynSolutionWrapper.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -internal interface IRoslynSolutionWrapper -{ - IRoslynSolutionWrapper RemoveAnalyzerReferences(IReadOnlyCollection analyzers); - - IRoslynSolutionWrapper AddAnalyzerReferences(IReadOnlyCollection analyzers); - - Solution RoslynSolution { get; } - - string DisplayCurrentAnalyzerState(); - - bool ContainsAnalyzer(AnalyzerFileReference analyzer); -} - -[ExcludeFromCodeCoverage] -internal class RoslynSolutionWrapper(Solution solution) : IRoslynSolutionWrapper -{ - public bool ContainsAnalyzer(AnalyzerFileReference analyzer) => solution.AnalyzerReferences.Contains(analyzer); - - public IRoslynSolutionWrapper RemoveAnalyzerReferences(IReadOnlyCollection analyzers) => - new RoslynSolutionWrapper(analyzers.Aggregate(solution, (current, analyzer) => current.RemoveAnalyzerReference(analyzer))); - - public IRoslynSolutionWrapper AddAnalyzerReferences(IReadOnlyCollection analyzers) => - new RoslynSolutionWrapper(solution.AddAnalyzerReferences(analyzers)); - - public Solution RoslynSolution => solution; - - public string DisplayCurrentAnalyzerState() - { - try - { - var stringBuilder = new StringBuilder(); - - stringBuilder.AppendLine($"Solution {solution.FilePath}, {solution.Version} Analyzers"); - foreach (var currentSolutionAnalyzer in solution.AnalyzerReferences) - { - PrintAnalyzer(stringBuilder, currentSolutionAnalyzer); - } - - foreach (var projectId in solution.ProjectIds) - { - var project = solution.GetProject(projectId)!; - stringBuilder.AppendLine($"Project {project.Name} Analyzers"); - foreach (var projectAnalyzer in project.AnalyzerReferences) - { - PrintAnalyzer(stringBuilder, projectAnalyzer); - } - } - - return stringBuilder.ToString(); - } - catch - { - // this code is purely for debugging/investigating, so we don't care about exceptions here - return "Failed to list solution analyzers"; - } - } - - private static void PrintAnalyzer(StringBuilder stringBuilder, AnalyzerReference analyzer) => stringBuilder.AppendLine($" {analyzer.DisplayInfo()}"); -} - -internal static class AnalyzerReferenceExtensions -{ - public static string DisplayInfo(this AnalyzerReference analyzer) => $"{analyzer?.Display}, {analyzer?.Id}, {analyzer?.FullPath}"; -} diff --git a/src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs b/src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs deleted file mode 100644 index b740614d68..0000000000 --- a/src/Infrastructure.VS/Roslyn/IRoslynWorkspaceWrapper.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.VisualStudio.LanguageServices; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.SystemAbstractions; - -namespace SonarLint.VisualStudio.Infrastructure.VS.Roslyn; - -internal interface IAnalyzerChange -{ - ImmutableArray AnalyzersToAdd { get; } - ImmutableArray AnalyzersToRemove { get; } - - IRoslynSolutionWrapper Change(IRoslynSolutionWrapper solution); -} - -internal class AnalyzerChange(ImmutableArray analyzersToRemove, ImmutableArray analyzersToAdd) : IAnalyzerChange -{ - public ImmutableArray AnalyzersToAdd { get; } = analyzersToAdd; - public ImmutableArray AnalyzersToRemove { get; } = analyzersToRemove; - - public IRoslynSolutionWrapper Change(IRoslynSolutionWrapper solution) - { - var analyzersToRemoveFiltered = AnalyzersToRemove.RemoveAll(x => !solution.ContainsAnalyzer(x)); - if (analyzersToRemoveFiltered.Any()) - { - solution = solution.RemoveAnalyzerReferences(analyzersToRemoveFiltered); - } - var analyzersToAddFiltered = AnalyzersToAdd.RemoveAll(x => solution.ContainsAnalyzer(x)); - if (analyzersToAddFiltered.Any()) - { - solution = solution.AddAnalyzerReferences(analyzersToAddFiltered); - } - return solution; - } -} - -internal interface IRoslynWorkspaceWrapper -{ - IRoslynSolutionWrapper CurrentSolution { get; } - - Task TryApplyChangesAsync(IAnalyzerChange analyzerChange); -} - -[Export(typeof(IRoslynWorkspaceWrapper))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class RoslynWorkspaceWrapper([Import(typeof(VisualStudioWorkspace))] Workspace workspace, IThreadHandling threadHandling) : IRoslynWorkspaceWrapper -{ - private const int ApplyRetryCount = 5; - - public IRoslynSolutionWrapper CurrentSolution => - new RoslynSolutionWrapper(workspace.CurrentSolution); - - public async Task TryApplyChangesAsync(IAnalyzerChange analyzerChange) - { - for (var attempt = 0; attempt < ApplyRetryCount; attempt++) - { - if (await TryApplyChangesInternalAsync(analyzerChange) is {} result) - { - return result; - } - } - - return null; - } - - private async Task TryApplyChangesInternalAsync(IAnalyzerChange analyzerChange) - { - IRoslynSolutionWrapper wasApplied = null; - await threadHandling.RunOnUIThreadAsync(() => - { - var currentSolution = CurrentSolution; - var updatedSolution = analyzerChange.Change(currentSolution); - - wasApplied = updatedSolution == currentSolution || workspace.TryApplyChanges(updatedSolution.RoslynSolution) ? CurrentSolution : null; - }); - return wasApplied; - } -} diff --git a/src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs deleted file mode 100644 index 2d9434bbe1..0000000000 --- a/src/Integration.UnitTests/CSharpVB/Install/ImportsBeforeFileGeneratorTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Reflection; -using System.Xml; -using NSubstitute.ExceptionExtensions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Integration.CSharpVB.Install; -using SonarLint.VisualStudio.Integration.Resources; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB.Install; - -[TestClass] -public class ImportsBeforeFileGeneratorTests -{ - private static readonly string PathToDirectory = GetPathToImportBefore(); - private static readonly string PathToFile = Path.Combine(PathToDirectory, "SonarLint.targets"); - private const string ResourceContent = "some content"; - private IFileSystemService fileSystem; - private ILogger logger; - private ImportBeforeFileGenerator testSubject; - private IThreadHandling threadHandling; - private IEmbeddedResourceReader embeddedResourceReader; - - [TestInitialize] - public void TestInitialize() - { - logger = Substitute.For(); - fileSystem = Substitute.For(); - threadHandling = Substitute.For(); - embeddedResourceReader = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); - - testSubject = new ImportBeforeFileGenerator(logger, fileSystem, threadHandling, embeddedResourceReader); - } - - [TestMethod] - public void MefCtor_CheckExports() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void Mef_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Logger_LogContextIsSet() => logger.Received(1).ForContext(Strings.ImportsBeforeFileGeneratorLogContext); - - [TestMethod] - public async Task UpdateOrCreateTargetsFileAsync_RunsOnBackgroundThread() - { - await testSubject.UpdateOrCreateTargetsFileAsync(); - - threadHandling.ReceivedCalls().Should().ContainSingle(x => x.GetMethodInfo().Name == nameof(IThreadHandling.RunOnBackgroundThread)); - } - - [TestMethod] - public void FileDoesNotExist_CreatesFileWithWritesCorrectContent() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, ResourceContent, exists: false); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.Received(1).WriteAllText(PathToFile, ResourceContent); - } - - [TestMethod] - public void FileExists_DifferentText_CreatesFile() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, "wrong text"); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.Received(1).WriteAllText(PathToFile, ResourceContent); - } - - [TestMethod] - public void FileExists_SameText_DoesNotCreateFile() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, ResourceContent); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.DidNotReceive().WriteAllText(PathToFile, Arg.Any()); - } - - [TestMethod] - public void FileExists_EmbeddedResourceCanNotBeRead_DoesNotUpdateFileAndLogs() - { - MockEmbeddedResourceReader(content: null); - MockDirectoryExists(PathToDirectory, exists: true); - MockReadAllText(PathToFile, ResourceContent); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.File.DidNotReceive().WriteAllText(PathToFile, Arg.Any()); - logger.Received(1).LogVerbose(Strings.ImportBeforeFileGenerator_ContentOfTargetsFileCanNotBeRead, "SonarLint.targets"); - } - - [TestMethod] - public void DirectoryDoesNotExist_CreatesDirectory() - { - MockEmbeddedResourceReader(ResourceContent); - MockDirectoryExists(PathToDirectory, exists: false); - MockFileExists(PathToFile, exists: false); - - testSubject.UpdateOrCreateTargetsFile(); - - fileSystem.Directory.Received(1).CreateDirectory(PathToDirectory); - } - - [TestMethod] - public void ThrowsNonCriticalException_Catches() - { - fileSystem.Directory.Exists(Arg.Any()).Throws(new NotImplementedException("this is a test")); - - testSubject.UpdateOrCreateTargetsFile(); - - logger.Received(1).WriteLine(Arg.Is(x => x.Contains(Strings.ImportBeforeFileGenerator_FailedToWriteFile)), "this is a test"); - } - - [TestMethod] - public void ThrowsCriticalException_ThrowsException() - { - fileSystem.Directory.Exists(Arg.Any()).Throws(new StackOverflowException()); - - var act = () => testSubject.UpdateOrCreateTargetsFile(); - - act.Should().Throw(); - } - - [TestMethod] - public void ConvertResourceToXml_DoesNotThrow() - { - var fileContent = GetTargetFileContent(); - - var xmlDoc = new XmlDocument(); - var act = () => xmlDoc.LoadXml(fileContent); - - act.Should().NotThrow(); - } - - private static string GetPathToImportBefore() - { - var localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA"); - var pathToImportsBefore = Path.Combine(localAppData, "Microsoft", "MSBuild", "Current", "Microsoft.Common.targets", "ImportBefore"); - - return pathToImportsBefore; - } - - private static string GetTargetFileContent() - { - var resourcePath = "SonarLint.VisualStudio.Integration.CSharpVB.Install.SonarLintTargets.xml"; - using var stream = new StreamReader(typeof(ImportBeforeFileGenerator).Assembly.GetManifestResourceStream(resourcePath)); - - return stream.ReadToEnd(); - } - - private void MockDirectoryExists(string path, bool exists) => fileSystem.Directory.Exists(path).Returns(exists); - - private void MockFileExists(string path, bool exists) => fileSystem.File.Exists(path).Returns(exists); - - private void MockReadAllText(string path, string content, bool exists = true) - { - MockFileExists(path, exists); - fileSystem.File.ReadAllText(path).Returns(content); - } - - private void MockEmbeddedResourceReader(string content) => embeddedResourceReader.Read(Arg.Any(), Arg.Any()).Returns(content); -} diff --git a/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs b/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs deleted file mode 100644 index 4ddd7be439..0000000000 --- a/src/Integration.UnitTests/CSharpVB/SonarLintConfigGeneratorTests.cs +++ /dev/null @@ -1,274 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.CSharpVB; -using SonarLint.VisualStudio.Integration.CSharpVB; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Integration.UnitTests.CSharpVB; - -[TestClass] -public class SonarLintConfigGeneratorTests -{ - private SonarLintConfigGenerator testSubject; - private static readonly IEnumerable EmptyRules = Array.Empty(); - private static readonly IDictionary EmptyProperties = new Dictionary(); - private static readonly Language ValidLanguage = Language.CSharp; - private static readonly IReadOnlyDictionary ValidParams = new Dictionary { { "any", "any value" } }; - - [TestInitialize] - public void TestInitialize() => testSubject = new SonarLintConfigGenerator(LanguageProvider.Instance); - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => - MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public void Generate_NullArguments_Throws() - { - Action act = () => testSubject.Generate(null, EmptyProperties, new ServerExclusions(), ValidLanguage); - act.Should().ThrowExactly().And.ParamName.Should().Be("rules"); - - act = () => testSubject.Generate(EmptyRules, null, new ServerExclusions(), ValidLanguage); - act.Should().ThrowExactly().And.ParamName.Should().Be("sonarProperties"); - - act = () => testSubject.Generate(EmptyRules, EmptyProperties, null, ValidLanguage); - act.Should().ThrowExactly().And.ParamName.Should().Be("fileExclusions"); - - act = () => testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), null); - act.Should().ThrowExactly().And.ParamName.Should().Be("language"); - } - - [TestMethod] - [DataRow("xxx")] - [DataRow("CS")] // should be case-sensitive - [DataRow("vb")] // VB language key is "vbnet" - public void Generate_UnrecognisedLanguage_Throws(string languageKey) - { - Action act = () => testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), - new Language(languageKey, "languageX", languageKey, new PluginInfo("pluginKey", null), new RepoInfo("repoKey"))); - act.Should().ThrowExactly().And.ParamName.Should().Be("language"); - } - - [TestMethod] - [DataRow("cs")] - [DataRow("vbnet")] - public void Generate_NoActiveRulesOrSettings_ValidLanguage_ReturnsValidConfig(string languageKey) - { - var actual = testSubject.Generate(EmptyRules, EmptyProperties, new ServerExclusions(), ToLanguage(languageKey)); - - actual.Should().NotBeNull(); - actual.Rules.Should().BeEmpty(); - actual.Settings.Should().BeEmpty(); - } - - [TestMethod] - public void Generate_ValidSettings_OnlyLanguageSpecificSettingsReturned() - { - // Arrange - var properties = new Dictionary - { - { "sonar.cs.property1", "valid setting 1" }, - { "sonar.cs.property2", "valid setting 2" }, - { "sonar.vbnet.property1", "wrong language - not returned" }, - { "sonar.CS.property2", "wrong case - not returned" }, - { "sonar.cs.", "incorrect prefix - not returned" }, - { "xxx.cs.property1", "key does not match - not returned" }, - { ".does.not.match", "not returned" } - }; - - // Act - var actual = testSubject.Generate(EmptyRules, properties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Settings.Should().BeEquivalentTo(new Dictionary { { "sonar.cs.property1", "valid setting 1" }, { "sonar.cs.property2", "valid setting 2" } }); - } - - [TestMethod] - public void Generate_ValidSettings_AreSorted() - { - // Arrange - var properties = new Dictionary { { "sonar.cs.property3", "aaa" }, { "sonar.cs.property1", "bbb" }, { "sonar.cs.property2", "ccc" }, }; - - // Act - var actual = testSubject.Generate(EmptyRules, properties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Settings[0].Key.Should().Be("sonar.cs.property1"); - actual.Settings[0].Value.Should().Be("bbb"); - - actual.Settings[1].Key.Should().Be("sonar.cs.property2"); - actual.Settings[1].Value.Should().Be("ccc"); - - actual.Settings[2].Key.Should().Be("sonar.cs.property3"); - actual.Settings[2].Value.Should().Be("aaa"); - } - - [TestMethod] - public void Generate_ValidSettings_SecuredSettingsAreNotReturned() - { - // Arrange - var properties = new Dictionary - { - { "sonar.cs.property1.secured", "secure - should not be returned" }, - { "sonar.cs.property2", "valid setting" }, - { "sonar.cs.property3.SECURED", "secure - should not be returned2" }, - }; - - // Act - var actual = testSubject.Generate(EmptyRules, properties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Settings.Should().BeEquivalentTo(new Dictionary { { "sonar.cs.property2", "valid setting" } }); - } - - [TestMethod] - [DataRow("cs", "csharpsquid")] - [DataRow("vbnet", "vbnet")] - public void Generate_ValidRules_OnlyRulesFromKnownRepositoryReturned(string knownLanguageKey, string knownRepoKey) - { - // Arrange - var rules = new List() - { - CreateRuleWithValidParams("valid1", knownRepoKey), - CreateRuleWithValidParams("unknown1", "unknown.repo.key"), - CreateRuleWithValidParams("valid2", knownRepoKey), - CreateRuleWithValidParams("invalid2", "another.unknown.repo.key"), - CreateRuleWithValidParams("valid3", knownRepoKey) - }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), ToLanguage(knownLanguageKey)); - - // Assert - actual.Rules.Select(r => r.Key).Should().BeEquivalentTo(new string[] { "valid1", "valid2", "valid3" }); - } - - [TestMethod] - [DataRow("cs")] - [DataRow("vbnet")] - public void Generate_SonarSecurityRules_AreNotReturned(string languageKey) - { - // Arrange - var rules = new List() - { - CreateRuleWithValidParams("valid1", $"roslyn.sonaranalyzer.security.{languageKey}"), CreateRuleWithValidParams("valid2", $"roslyn.sonaranalyzer.security.{languageKey}") - }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), ToLanguage(languageKey)); - - // Assert - actual.Rules.Should().BeEmpty(); - } - - [TestMethod] - public void Generate_ValidRules_OnlyRulesWithParametersReturned() - { - // Arrange - var rule1Params = new Dictionary { { "param1", "value1" }, { "param2", "value2" } }; - var rule3Params = new Dictionary { { "param3", "value4" } }; - - var rules = new List() { CreateRule("s111", "csharpsquid", rule1Params), CreateRule("s222", "csharpsquid" /* no params */), CreateRule("s333", "csharpsquid", rule3Params) }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Rules.Count.Should().Be(2); - - actual.Rules[0].Key.Should().Be("s111"); - actual.Rules[0].Parameters.Should().BeEquivalentTo(rule1Params); - actual.Rules[1].Key.Should().Be("s333"); - actual.Rules[1].Parameters.Should().BeEquivalentTo(rule3Params); - } - - [TestMethod] - public void Generate_ValidRules_AreSorted() - { - // Arrange - var rules = new List() - { - CreateRule("s222", "csharpsquid", - new Dictionary { { "any", "any" } }), - CreateRule("s111", "csharpsquid", - new Dictionary { { "CCC", "value 1" }, { "BBB", "value 2" }, { "AAA", "value 3" } }), - CreateRule("s333", "csharpsquid", - new Dictionary { { "any", "any" } }) - }; - - // Act - var actual = testSubject.Generate(rules, EmptyProperties, new ServerExclusions(), Language.CSharp); - - // Assert - actual.Rules.Count.Should().Be(3); - - actual.Rules[0].Key.Should().Be("s111"); - actual.Rules[1].Key.Should().Be("s222"); - actual.Rules[2].Key.Should().Be("s333"); - - actual.Rules[0].Parameters[0].Key.Should().Be("AAA"); - actual.Rules[0].Parameters[0].Value.Should().Be("value 3"); - - actual.Rules[0].Parameters[1].Key.Should().Be("BBB"); - actual.Rules[0].Parameters[1].Value.Should().Be("value 2"); - - actual.Rules[0].Parameters[2].Key.Should().Be("CCC"); - actual.Rules[0].Parameters[2].Value.Should().Be("value 1"); - } - - [TestMethod] - public void Generate_HasExclusions_ExclusionsIncludedInConfig() - { - var exclusions = new ServerExclusions( - exclusions: new[] { "**/path1", "**/*/path2" }, - globalExclusions: new[] { "**/path3" }, - inclusions: new[] { "**/path4" }); - - var actual = testSubject.Generate(EmptyRules, EmptyProperties, exclusions, Language.CSharp); - - actual.Settings.Count.Should().Be(3); - - actual.Settings[0].Should().BeEquivalentTo(new SonarLintKeyValuePair { Key = "sonar.exclusions", Value = "**/path1,**/*/path2" }); - actual.Settings[1].Should().BeEquivalentTo(new SonarLintKeyValuePair { Key = "sonar.global.exclusions", Value = "**/path3" }); - actual.Settings[2].Should().BeEquivalentTo(new SonarLintKeyValuePair { Key = "sonar.inclusions", Value = "**/path4" }); - } - - private static IRuleParameters CreateRuleWithValidParams(string ruleKey, string repoKey) => CreateRule(ruleKey, repoKey, ValidParams); - - private static IRuleParameters CreateRule(string ruleKey, string repoKey, IReadOnlyDictionary parameters = null) - { - var ruleParameters = Substitute.For(); - ruleParameters.Key.Returns(ruleKey); - ruleParameters.RepositoryKey.Returns(repoKey); - ruleParameters.Parameters.Returns(parameters); - - return ruleParameters; - } - - private static Language ToLanguage(string sqLanguageKey) => LanguageProvider.Instance.GetLanguageFromLanguageKey(sqLanguageKey); -} diff --git a/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs b/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs index deb4905f11..9b11a33571 100644 --- a/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs +++ b/src/Integration.Vsix.UnitTests/Analysis/MuteIssueCommandTests.cs @@ -213,7 +213,7 @@ public void Execute_WhenNoIssueSelected_DoesNothing() [TestMethod] public void Execute_WithRoslynIssue_MutesIssue() { - var roslynIssue = SetupRoslynIssue(out var sonarQubeIssue); + var roslynIssue = SetupRoslynIssue(); testSubjectMenuCommand.Invoke(); @@ -268,7 +268,7 @@ public void Execute_WithNonRoslynIssue_WhenMuteIssueException_CatchesAndShowsMes [TestMethod] public void Execute_WithRoslynIssue_WhenMuteIssueException_CatchesAndShowsMessageBox() { - SetupRoslynIssue(out _); + SetupRoslynIssue(); muteIssuesService.ResolveIssueWithDialogAsync(Arg.Any()).ThrowsAsync(new MuteIssueException("Error while muting")); var act = () => testSubjectMenuCommand.Invoke(); @@ -292,7 +292,7 @@ public void Execute_WithNonRoslynIssue_WhenMuteIssueCommentFailedException_Catch [TestMethod] public void Execute_WithRoslynIssue_WhenMuteIssueCommentFailedException_CatchesAndShowsMessageBox() { - SetupRoslynIssue(out _); + SetupRoslynIssue(); muteIssuesService.ResolveIssueWithDialogAsync(Arg.Any()).ThrowsAsync(new MuteIssueException.MuteIssueCommentFailedException()); var act = () => testSubjectMenuCommand.Invoke(); @@ -316,7 +316,7 @@ public void Execute_WithNonRoslynIssue_WhenMuteIssueCancelledException_Catches() [TestMethod] public void Execute_WithRoslynIssue_WhenMuteIssueCancelledException_Catches() { - SetupRoslynIssue(out _); + SetupRoslynIssue(); muteIssuesService.ResolveIssueWithDialogAsync(Arg.Any()).ThrowsAsync(new MuteIssueException.MuteIssueCancelledException()); var act = () => testSubjectMenuCommand.Invoke(); @@ -333,10 +333,9 @@ public void SupportedRepos_AllKnownLanguagesAreSupported() supportedRepos.Should().HaveCount(languageProvider.AllKnownLanguages.Count); } - private IFilterableRoslynIssue SetupRoslynIssue(out SonarQubeIssue sonarQubeIssue) + private IFilterableRoslynIssue SetupRoslynIssue() { var roslynIssue = Substitute.For(); - sonarQubeIssue = DummySonarQubeIssueFactory.CreateServerIssue(); errorListHelper.TryGetIssueFromSelectedRow(out _).Returns(x => { x[0] = roslynIssue; diff --git a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs index c49d8044aa..5359747443 100644 --- a/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs +++ b/src/Integration.Vsix.UnitTests/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocatorTests.cs @@ -61,86 +61,6 @@ public void MefCtor_CheckIsExported() => [TestMethod] public void MefCtor_IsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - [TestMethod] - public void GetBasicAnalyzerFullPaths_AnalyzersExists_ReturnsFullPathsToAnalyzers() - { - string[] expectedPaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(expectedPaths); - - var paths = testSubject.GetBasicAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(expectedPaths); - } - - [TestMethod] - public void GetBasicAnalyzerFullPaths_EnterpriseAnalyzersExists_Skips() - { - string[] filePaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(filePaths); - - var paths = testSubject.GetBasicAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(filePaths[0]); - } - - [TestMethod] - public void GetBasicAnalyzerFullPaths_SearchesForFilesInsideVsix() - { - vsixRootLocator.GetVsixRoot().Returns(PathInsideVsix); - - testSubject.GetBasicAnalyzerFullPaths(); - - fileSystem.Directory.Received(1).GetFiles(Path.Combine(PathInsideVsix, "EmbeddedDotnetAnalyzerDLLs"), "SonarAnalyzer.*.dll"); - } - - [TestMethod] - public void GetEnterpriseAnalyzerFullPaths_AnalyzersExists_ReturnsFullPathsToAnalyzers() - { - string[] expectedPaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(expectedPaths); - - var paths = testSubject.GetEnterpriseAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(expectedPaths); - } - - [TestMethod] - public void GetEnterpriseAnalyzerFullPaths_EnterpriseAnalyzersExists_ReturnsFullPathsToAnalyzers() - { - string[] expectedPaths = - [ - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.1.dll"), - GetAnalyzerFullPath(PathInsideVsix, "SonarAnalyzer.Enterprise.2.dll") - ]; - fileSystem.Directory.GetFiles(Arg.Any(), Arg.Any()).Returns(expectedPaths); - - var paths = testSubject.GetEnterpriseAnalyzerFullPaths(); - - paths.Should().BeEquivalentTo(expectedPaths); - } - - [TestMethod] - public void GetAnalyzerFullPaths_SearchesForFilesInsideVsix() - { - vsixRootLocator.GetVsixRoot().Returns(PathInsideVsix); - - testSubject.GetEnterpriseAnalyzerFullPaths(); - - fileSystem.Directory.Received(1).GetFiles(Path.Combine(PathInsideVsix, "EmbeddedDotnetAnalyzerDLLs"), "SonarAnalyzer.*.dll"); - } - [TestMethod] public void GetAnalyzerFullPathsByLanguage_ReturnsExpectedPaths() { diff --git a/src/Integration.Vsix.UnitTests/packages.lock.json b/src/Integration.Vsix.UnitTests/packages.lock.json index 75045521bd..1a82191461 100644 --- a/src/Integration.Vsix.UnitTests/packages.lock.json +++ b/src/Integration.Vsix.UnitTests/packages.lock.json @@ -1366,7 +1366,6 @@ "SonarLint.VisualStudio.Integration": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", - "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", @@ -1483,18 +1482,6 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" - } - }, "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { diff --git a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs index 46c8e4f05d..24fba3f83c 100644 --- a/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs +++ b/src/Integration.Vsix/EmbeddedAnalyzers/EmbeddedDotnetAnalyzersLocator.cs @@ -23,18 +23,16 @@ using System.IO.Abstractions; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Infrastructure.VS.Roslyn; using SonarLint.VisualStudio.Integration.Vsix.Helpers; using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis.Configuration; namespace SonarLint.VisualStudio.Integration.Vsix.EmbeddedAnalyzers; [Export(typeof(IEmbeddedDotnetAnalyzersLocator))] -[Export(typeof(IObsoleteDotnetAnalyzersLocator))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, ILanguageProvider languageProvider, IFileSystemService fileSystem) - : IEmbeddedDotnetAnalyzersLocator, IObsoleteDotnetAnalyzersLocator + : IEmbeddedDotnetAnalyzersLocator { private const string PathInsideVsix = "EmbeddedDotnetAnalyzerDLLs"; private const string DllsSearchPattern = "SonarAnalyzer.*.dll"; // starting from 10.0, the analyzer assemblies are merged and all of the dll names start with SonarAnalyzer @@ -42,8 +40,6 @@ internal class EmbeddedDotnetAnalyzersLocator(IVsixRootLocator vsixRootLocator, private readonly IFileSystem fileSystem = fileSystem; - public List GetBasicAnalyzerFullPaths() => GetBasicAnalyzerDlls().ToList(); - public Dictionary> GetAnalyzerFullPathsByLicensedLanguage() { var languageToDllsMap = new Dictionary>(); @@ -66,10 +62,6 @@ private static List GetAnalyzerFullPathsByLanguage(RoslynLanguage langua return dlls.Where(dll => dll.Contains(language.RoslynDllIdentifier)).ToList(); } - private IEnumerable GetBasicAnalyzerDlls() => GetAllAnalyzerDlls().Where(x => !x.Contains(EnterpriseInfix)); - - public List GetEnterpriseAnalyzerFullPaths() => GetAllAnalyzerDlls().ToList(); - private string[] GetAllAnalyzerDlls() => fileSystem.Directory.GetFiles(GetPathToParentFolder(), DllsSearchPattern); private string GetPathToParentFolder() => Path.Combine(vsixRootLocator.GetVsixRoot(), PathInsideVsix); diff --git a/src/Integration.Vsix/Integration.Vsix.csproj b/src/Integration.Vsix/Integration.Vsix.csproj index a93a2827bc..c211951346 100644 --- a/src/Integration.Vsix/Integration.Vsix.csproj +++ b/src/Integration.Vsix/Integration.Vsix.csproj @@ -110,12 +110,6 @@ BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b DebugSymbolsProjectOutputGroup%3b - - {082D5D8E-F914-4139-9AE3-3F48B679E3DA} - Roslyn.Suppressions - BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b - DebugSymbolsProjectOutputGroup%3b - {58619C0F-0F3D-4E8C-B204-A19B332D45E5} CFamily diff --git a/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest b/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest index 0d2d928242..a439a6f975 100644 --- a/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest +++ b/src/Integration.Vsix/Manifests/VS2022/source.extension.vsixmanifest @@ -41,18 +41,6 @@ - - - - - - - - diff --git a/src/Integration.Vsix/SonarLintDaemonPackage.cs b/src/Integration.Vsix/SonarLintDaemonPackage.cs index 9950f7436e..96f42ee149 100644 --- a/src/Integration.Vsix/SonarLintDaemonPackage.cs +++ b/src/Integration.Vsix/SonarLintDaemonPackage.cs @@ -27,7 +27,6 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.CFamily; -using SonarLint.VisualStudio.Integration.CSharpVB.Install; using SonarLint.VisualStudio.Integration.Vsix.Analysis; using SonarLint.VisualStudio.Integration.Vsix.Events; using SonarLint.VisualStudio.Integration.Vsix.Resources; @@ -120,9 +119,6 @@ private async Task InitAsync() projectDocumentsEventsListener = await this.GetMefServiceAsync(); projectDocumentsEventsListener.Initialize(); - var importBeforeFileGenerator = await this.GetMefServiceAsync(); - importBeforeFileGenerator.UpdateOrCreateTargetsFileAsync().Forget(); - var thread = await this.GetMefServiceAsync(); var roslynAnalyzerAssemblyLoader = await this.GetMefServiceAsync(); roslynAnalysisHttpServer = await this.GetMefServiceAsync(); diff --git a/src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs b/src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs deleted file mode 100644 index f97b64c057..0000000000 --- a/src/Integration/CSharpVB/Install/ImportBeforeFileGenerator.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.SystemAbstractions; -using SonarLint.VisualStudio.Integration.Resources; - -namespace SonarLint.VisualStudio.Integration.CSharpVB.Install; - -/// -/// Creates a .targets file in the ImportBefore directory with the contents of the SonarLintTargets.xml file and updates it when the content becomes outdated. -/// -public interface IImportBeforeFileGenerator -{ - Task UpdateOrCreateTargetsFileAsync(); -} - -[Export(typeof(IImportBeforeFileGenerator))] -[PartCreationPolicy(CreationPolicy.Shared)] -[method: ImportingConstructor] -internal class ImportBeforeFileGenerator( - ILogger logger, - IFileSystemService fileSystem, - IThreadHandling threadHandling, - IEmbeddedResourceReader embeddedResourceReader) : IImportBeforeFileGenerator -{ - private readonly ILogger logger = logger.ForContext(Strings.ImportsBeforeFileGeneratorLogContext); - private const string TargetsFileName = "SonarLint.targets"; - private const string ResourcePath = "SonarLint.VisualStudio.Integration.CSharpVB.Install.SonarLintTargets.xml"; - - private static readonly object Locker = new(); - - public Task UpdateOrCreateTargetsFileAsync() => threadHandling.RunOnBackgroundThread(UpdateOrCreateTargetsFile); - - internal void UpdateOrCreateTargetsFile() - { - lock (Locker) - { - var fileContent = embeddedResourceReader.Read(GetType().Assembly, ResourcePath); - var pathToImportBefore = GetPathToImportBefore(); - var fullPath = Path.Combine(pathToImportBefore, TargetsFileName); - - logger.LogVerbose(Strings.ImportBeforeFileGenerator_CheckingIfFileExists, fullPath); - - try - { - if (fileContent == null) - { - logger.LogVerbose(Strings.ImportBeforeFileGenerator_ContentOfTargetsFileCanNotBeRead, TargetsFileName); - return; - } - - if (!fileSystem.Directory.Exists(pathToImportBefore)) - { - logger.LogVerbose(Strings.ImportBeforeFileGenerator_CreatingDirectory, pathToImportBefore); - fileSystem.Directory.CreateDirectory(pathToImportBefore); - } - - if (fileSystem.File.Exists(fullPath) && fileSystem.File.ReadAllText(fullPath) == fileContent) - { - logger.LogVerbose(Strings.ImportBeforeFileGenerator_FileAlreadyExists); - return; - } - - logger.LogVerbose(Strings.ImportBeforeFileGenerator_WritingTargetFileToDisk); - fileSystem.File.WriteAllText(fullPath, fileContent); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.WriteLine(Strings.ImportBeforeFileGenerator_FailedToWriteFile, ex.Message); - logger.LogVerbose(Strings.ImportBeforeFileGenerator_FailedToWriteFile_Verbose, ex.ToString()); - } - } - } - - private static string GetPathToImportBefore() - { - var localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA"); - var pathToImportBefore = Path.Combine(localAppData, "Microsoft", "MSBuild", "Current", "Microsoft.Common.targets", "ImportBefore"); - - return pathToImportBefore; - } -} diff --git a/src/Integration/CSharpVB/Install/SonarLintTargets.xml b/src/Integration/CSharpVB/Install/SonarLintTargets.xml deleted file mode 100644 index e718e5a342..0000000000 --- a/src/Integration/CSharpVB/Install/SonarLintTargets.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - <_SLVSConnectedModeFolder>$(APPDATA)\SonarLint for Visual Studio\Bindings\$(SolutionName) - <_SLVSStandaloneModeFolder>$(APPDATA)\SonarLint for Visual Studio\SolutionSettings\$(SolutionName) - <_SLVSStandaloneModeSettingsJson>$(_SLVSStandaloneModeFolder)\settings.json - - <_SLVSRootFolder>$(APPDATA)\SonarLint for Visual Studio\.global - <_SLVSRootFolder Condition=" Exists($(_SLVSStandaloneModeSettingsJson))">$(_SLVSStandaloneModeFolder) - <_SLVSRootFolder Condition=" Exists($(_SLVSConnectedModeFolder))">$(_SLVSConnectedModeFolder) - - - <_SonarLanguage>$(Language.ToLowerInvariant) - <_SonarLanguage Condition=" $(Language) == 'c#'">csharp - <_SonarLanguage Condition=" $(Language) == 'visualbasic'">vb - - <_SLVSGeneratedRoslynConfigFile>$(_SLVSRootFolder)\sonarlint_$(_SonarLanguage).globalconfig - <_SLVSGeneratedAdditionalFile>$(_SLVSRootFolder)\$(_SonarLanguage)\SonarLint.xml - - - - - - - false - - - diff --git a/src/Integration/Integration.csproj b/src/Integration/Integration.csproj index 09467b5212..d05fec3f81 100644 --- a/src/Integration/Integration.csproj +++ b/src/Integration/Integration.csproj @@ -58,7 +58,6 @@ - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs deleted file mode 100644 index 36ac96b201..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/IssueConverterTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess -{ - [TestClass] - public class IssueConverterTests - { - [TestMethod] - public void Convert_SimplePropertiesAreHandledCorrectly() - { - var sonarIssue = CreateSonarQubeIssue(hash: "aaaa", filePath: "\\bbb\\ccc\\file.txt", issueKey: "key"); - - var actual = IssueConverter.Convert(sonarIssue); - - actual.Hash.Should().Be("aaaa"); - actual.FilePath.Should().Be("\\bbb\\ccc\\file.txt"); - actual.IssueServerKey.Should().Be("key"); - } - - [TestMethod] - [DataRow(1, 0)] - [DataRow(2, 1)] - [DataRow(null, null)] - public void Convert_LineNumbersAreHandledCorrectly(int? sonarLineNumber, int? expected) - { - // 1-based Sonar line numbers should be converted to 0-based Roslyn line numbers - var sonarIssue = CreateSonarQubeIssue(line: sonarLineNumber); - - var actual = IssueConverter.Convert(sonarIssue); - - if (expected.HasValue) - { - actual.RoslynIssueLine.Value.Should().Be(expected); - } - else - { - actual.RoslynRuleId.Should().BeNull(); - } - } - - [TestMethod] - // Recognised repos - [DataRow("csharpsquid:S123", RoslynLanguage.CSharp, "S123")] - [DataRow("vbnet:S999", RoslynLanguage.VB, "S999")] - - // Valid but unrecognised repos - [DataRow("cpp:S111", RoslynLanguage.Unknown, "S111")] - [DataRow("javascript:S222", RoslynLanguage.Unknown, "S222")] - [DataRow("CSHARPSQUID:S333", RoslynLanguage.Unknown, "S333")] - [DataRow("VBNET:S444", RoslynLanguage.Unknown, "S444")] - - // Invalid keys - should not error - [DataRow("invalidcompositekey", RoslynLanguage.Unknown, null)] - [DataRow("invalid::compositekey", RoslynLanguage.Unknown, ":compositekey")] - [DataRow(":invalid", RoslynLanguage.Unknown, "invalid")] - [DataRow("csharpsquid:", RoslynLanguage.CSharp, "")] - public void Convert_RepoKeysAreHandledCorrectly(string compositeKey, RoslynLanguage expectedLanguage, string expectedRuleKey) - { - var sonarIssue = CreateSonarQubeIssue(ruleId: compositeKey); - - var actual = IssueConverter.Convert(sonarIssue); - - actual.RoslynLanguage.Should().Be(expectedLanguage); - actual.RoslynRuleId.Should().Be(expectedRuleKey); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs deleted file mode 100644 index ca7871e1fc..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/NewSuppressedIssuesCalculatorTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class NewSuppressedIssuesCalculatorTests : SuppressedIssuesCalculatorTestsBase -{ - [TestInitialize] - public void TestInitialize() - { - RoslynSettingsFileStorage = Substitute.For(); - Logger = Substitute.For(); - Logger.ForContext(Arg.Any()).Returns(Logger); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_IssueDoNotExist_IssuesAreConvertedAndFiltered() - { - var testSubject = CreateNewSuppressedIssuesCalculator([ - CsharpIssueSuppressed, // C# issue - VbNetIssueSuppressed, // VB issue - CppIssueSuppressed, // C++ issue - ignored - UnknownRepoIssue, // unrecognised repo - ignored - InvalidRepoKeyIssue, // invalid repo key - ignored - NoRuleIdIssue // invalid repo key (no rule id) - ignored - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_IssueDoNotExist_OnlySuppressedIssuesAreInSettings() - { - MockExistingSuppressionsOnSettingsFile(); - var testSubject = CreateNewSuppressedIssuesCalculator([ - CsharpIssueSuppressed, - VbNetIssueSuppressed, - CsharpIssueNotSuppressed, - VbNetIssueNotSuppressed, - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_IssuesExist_UpdatesCorrectly() - { - var newSonarQubeIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed }; - MockExistingSuppressionsOnSettingsFile(newSonarQubeIssues); - var testSubject = CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], newSonarQubeIssues); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_TwoNewIssuesAdded_DoesNotRemoveExistingOnes() - { - var newSonarQubeIssues = new[] { CreateSonarQubeIssue("csharpsquid:S666"), CreateSonarQubeIssue("vbnet:S666") }; - var existingIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed, }; - MockExistingSuppressionsOnSettingsFile(existingIssues); - var testSubject = CreateNewSuppressedIssuesCalculator(newSonarQubeIssues); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - var expectedIssues = existingIssues.Union(newSonarQubeIssues).ToArray(); - VerifyExpectedSuppressions([.. result], expectedIssues); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - } - - private NewSuppressedIssuesCalculator CreateNewSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => new(Logger, RoslynSettingsFileStorage, sonarQubeIssues); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs deleted file mode 100644 index 76184ae9d5..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorFactoryTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarLint.VisualStudio.TestInfrastructure; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class SuppressedIssuesCalculatorFactoryTests -{ - private IRoslynSettingsFileStorage roslynSettingsFileStorage; - private ILogger logger; - private SuppressedIssuesCalculatorFactory testSubject; - - [TestInitialize] - public void TestInitialize() - { - roslynSettingsFileStorage = Substitute.For(); - logger = Substitute.For(); - logger.ForContext(Arg.Any()).Returns(logger); - - testSubject = new SuppressedIssuesCalculatorFactory(logger, roslynSettingsFileStorage); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckTypeIsNonShared() => MefTestHelpers.CheckIsNonSharedMefComponent(); - - [TestMethod] - public void Ctor_SetsLogContext() => logger.Received(1).ForContext(SuppressedIssuesCalculatorFactory.SuppressedIssuesCalculatorLogContext); - - [TestMethod] - public void CreateAllSuppressedIssuesCalculator_CreatesAllSuppressedIssuesCalculator() - { - var sonarQubeIssues = new[] { CreateSonarQubeIssue("csharpsquid:S111") }; - - var calculator = testSubject.CreateAllSuppressedIssuesCalculator(sonarQubeIssues); - - calculator.Should().BeOfType(); - } - - [TestMethod] - public void CreateNewSuppressedIssuesCalculator_CreatesNewSuppressedIssuesCalculator() - { - var sonarQubeIssues = new[] { CreateSonarQubeIssue("csharpsquid:S111") }; - - var calculator = testSubject.CreateNewSuppressedIssuesCalculator(sonarQubeIssues); - - calculator.Should().BeOfType(); - } - - [TestMethod] - public void CreateSuppressedIssuesRemovedCalculator_CreatesSuppressedIssuesRemovedCalculator() - { - var issueServerKeys = new[] { Guid.NewGuid().ToString() }; - - var calculator = testSubject.CreateSuppressedIssuesRemovedCalculator(issueServerKeys); - - calculator.Should().BeOfType(); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs deleted file mode 100644 index 696572f90d..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class SuppressedIssuesCalculatorTests : SuppressedIssuesCalculatorTestsBase -{ - [TestInitialize] - public void TestInitialize() - { - RoslynSettingsFileStorage = Substitute.For(); - Logger = Substitute.For(); - Logger.ForContext(Arg.Any()).Returns(Logger); - } - - [TestMethod] - public void AllSuppressedIssuesCalculator_IssuesAreConvertedAndFiltered() - { - var testSubject = CreateAllSuppressedIssuesCalculator([ - CsharpIssueSuppressed, // C# issue - VbNetIssueSuppressed, // VB issue - CppIssueSuppressed, // C++ issue - ignored - UnknownRepoIssue, // unrecognised repo - ignored - InvalidRepoKeyIssue, // invalid repo key - ignored - NoRuleIdIssue // invalid repo key (no rule id) - ignored - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerReloadSuppressions); - } - - [TestMethod] - public void AllSuppressedIssuesCalculator_OnlySuppressedIssuesAreInSettings() - { - var testSubject = CreateAllSuppressedIssuesCalculator([ - CsharpIssueSuppressed, - VbNetIssueSuppressed, - CsharpIssueNotSuppressed, - VbNetIssueNotSuppressed, - ]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [CsharpIssueSuppressed, VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerReloadSuppressions); - } - - private AllSuppressedIssuesCalculator CreateAllSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => new(Logger, sonarQubeIssues); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs deleted file mode 100644 index 2229430f59..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesCalculatorTestsBase.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -public class SuppressedIssuesCalculatorTestsBase -{ - internal const string RoslynSettingsKey = "my solution"; - internal IRoslynSettingsFileStorage RoslynSettingsFileStorage; - internal ILogger Logger; - - protected readonly SonarQubeIssue CsharpIssueSuppressed = CreateSonarQubeIssue("csharpsquid:S111"); - protected readonly SonarQubeIssue VbNetIssueSuppressed = CreateSonarQubeIssue("vbnet:S222"); - protected readonly SonarQubeIssue CppIssueSuppressed = CreateSonarQubeIssue("cpp:S333"); - protected readonly SonarQubeIssue UnknownRepoIssue = CreateSonarQubeIssue("xxx:S444"); - protected readonly SonarQubeIssue InvalidRepoKeyIssue = CreateSonarQubeIssue("xxxS555"); - protected readonly SonarQubeIssue NoRuleIdIssue = CreateSonarQubeIssue("xxx:"); - protected readonly SonarQubeIssue CsharpIssueNotSuppressed = CreateSonarQubeIssue("csharpsquid:S333", isSuppressed: false); - protected readonly SonarQubeIssue VbNetIssueNotSuppressed = CreateSonarQubeIssue("vbnet:S444", isSuppressed: false); - - internal static void VerifyExpectedSuppressions(SuppressedIssue[] actualSuppressions, SonarQubeIssue[] expectedSuppressions) - { - actualSuppressions.Should().HaveCount(expectedSuppressions.Length); - actualSuppressions.Should().BeEquivalentTo(expectedSuppressions.Select(IssueConverter.Convert)); - } - - protected void MockExistingSuppressionsOnSettingsFile(params SonarQubeIssue[] existingIssues) => - RoslynSettingsFileStorage.Get(Arg.Any()).Returns(existingIssues.Length == 0 - ? RoslynSettings.Empty - : new RoslynSettings { SonarProjectKey = RoslynSettingsKey, Suppressions = existingIssues.Select(IssueConverter.Convert) }); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs deleted file mode 100644 index 7eec1dad57..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/InProcess/SuppressedIssuesRemovedCalculatorTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.InProcess; - -[TestClass] -public class SuppressedIssuesRemovedCalculatorTests : SuppressedIssuesCalculatorTestsBase -{ - [TestInitialize] - public void TestInitialize() - { - RoslynSettingsFileStorage = Substitute.For(); - Logger = Substitute.For(); - Logger.ForContext(Arg.Any()).Returns(Logger); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_IssueDoNotExistInFile_DoesNotUpdateFile() - { - MockExistingSuppressionsOnSettingsFile(); - var newSonarQubeIssues = new[] - { - CsharpIssueSuppressed.IssueKey, // C# issue - VbNetIssueSuppressed.IssueKey, // VB issue - CppIssueSuppressed.IssueKey, // C++ issue - ignored - UnknownRepoIssue.IssueKey, // unrecognised repo - ignored - InvalidRepoKeyIssue.IssueKey, // invalid repo key - ignored - NoRuleIdIssue.IssueKey // invalid repo key (no rule id) - ignored - }; - var testSubject = CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - result.Should().BeNull(); - RoslynSettingsFileStorage.DidNotReceiveWithAnyArgs().Update(default, default); - Logger.DidNotReceive().LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_IssueKeysExistInFile_RemovesIssues() - { - var newSonarQubeIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed }; - MockExistingSuppressionsOnSettingsFile(newSonarQubeIssues); - var testSubject = CreateSuppressedIssuesRemovedCalculator(newSonarQubeIssues.Select(x => x.IssueKey).ToArray()); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], []); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_OneNewIssueResolved_DoesNotRemoveExistingOne() - { - var existingIssues = new[] { CsharpIssueSuppressed, VbNetIssueSuppressed, }; - MockExistingSuppressionsOnSettingsFile(existingIssues); - var testSubject = CreateSuppressedIssuesRemovedCalculator([CsharpIssueSuppressed.IssueKey]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], [VbNetIssueSuppressed]); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - [TestMethod] - public void SuppressedIssuesRemovedCalculator_MultipleIssuesWithSameIssueServerKeyExistsInFile_RemovesThemAll() - { - var existingIssues = new[] { CsharpIssueSuppressed, CreateSonarQubeIssue(issueKey: CsharpIssueSuppressed.IssueKey), CreateSonarQubeIssue(issueKey: CsharpIssueSuppressed.IssueKey) }; - MockExistingSuppressionsOnSettingsFile(existingIssues); - - var testSubject = CreateSuppressedIssuesRemovedCalculator([CsharpIssueSuppressed.IssueKey]); - - var result = testSubject.GetSuppressedIssuesOrNull(RoslynSettingsKey); - - VerifyExpectedSuppressions([.. result], []); - Logger.Received(1).LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - } - - private SuppressedIssuesRemovedCalculator CreateSuppressedIssuesRemovedCalculator(IEnumerable serverIssueKeys) => new(Logger, RoslynSettingsFileStorage, serverIssueKeys); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj deleted file mode 100644 index a38b04e5d5..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Roslyn.Suppressions.UnitTests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - {C478DAE7-58BC-4D02-929E-E413B40F2517} - SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests - SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests - - - - - - - - - - - TestParallelization.cs - - - - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs deleted file mode 100644 index 3a9ba5279e..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileInfoTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Globalization; -using System.IO; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarLint.VisualStudio.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class RoslynSettingsFileInfoTests - { - - [DataRow("projectkey", "projectkey")]//Testing lower case - [DataRow("projectKEY", "projectkey")]//Testing upper case - [DataRow("III", "iii")]//Testing upper case with invariant culture - [DataRow("project:key", "project_key")]//Testing illegal characters - [TestMethod] - public void GetSettingsFilePath_ReturnsFilePathCorrectly(string projectKey, string expectedFileName) - { - var expectedPath = Path.Combine(RoslynSettingsFileInfo.Directory, expectedFileName + ".json"); - - //This is to make sure normalising the keys done correctly with invariant culture - //https://en.wikipedia.org/wiki/Dotted_and_dotless_I - using var scope = new TurkishCultureScope(); - - var actualPath = RoslynSettingsFileInfo.GetSettingsFilePath(projectKey); - - actualPath.Should().Be(expectedPath); - - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs deleted file mode 100644 index 9fc744ee91..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileStorageTests.cs +++ /dev/null @@ -1,271 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO.Abstractions; -using Moq; -using Newtonsoft.Json; -using NSubstitute.ExceptionExtensions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.Resources; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarLint.VisualStudio.TestInfrastructure; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests; - -[TestClass] -public class RoslynSettingsFileStorageTests -{ - private const string SolutionName = "a solution name"; - private IFile file; - private IFileSystem fileSystem; - private TestLogger logger; - private RoslynSettingsFileStorage testSubject; - - [TestInitialize] - public void TestInitialize() - { - logger = new TestLogger(); - file = Substitute.For(); - fileSystem = Substitute.For(); - testSubject = new RoslynSettingsFileStorage(logger, fileSystem); - - MockFileSystem(); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - - [TestMethod] - public void Update_HasIssues_IssuesWrittenToFile() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = new[] { CreateIssue("issue1") } }; - - testSubject.Update(settings, SolutionName); - - CheckFileWritten(settings, SolutionName); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Get_HasIssues_IssuesReadFromFile() - { - var issue1 = CreateIssue("key1"); - var issue2 = CreateIssue("key2"); - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = new[] { issue1, issue2 } }; - MockFileReadAllText(settings); - - var actual = testSubject.Get("projectKey"); - - var issuesGotten = actual.Suppressions.ToList(); - - file.Received(1).ReadAllText(GetFilePath("projectKey")); - logger.AssertNoOutputMessages(); - - issuesGotten.Count.Should().Be(2); - issuesGotten[0].RoslynRuleId.Should().Be(issue1.RoslynRuleId); - issuesGotten[1].RoslynRuleId.Should().Be(issue2.RoslynRuleId); - } - - [TestMethod] - public void Update_SolutionNameHasInvalidChars_InvalidCharsReplaced() - { - var settings = new RoslynSettings { SonarProjectKey = "project:key" }; - - testSubject.Update(settings, "my:solution"); - - CheckFileWritten(settings, "my_solution"); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Update_ErrorOccuredWhenWritingFile_ErrorIsLogged() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey" }; - file.When(x => x.WriteAllText(Arg.Any(), Arg.Any())).Do(x => throw new Exception("Test Exception")); - - testSubject.Update(settings, "any"); - - logger.AssertOutputStrings("[Roslyn Suppressions] Error writing settings for project projectKey. Issues suppressed on the server may not be suppressed in the IDE. Error: Test Exception"); - } - - [TestMethod] - public void Get_ErrorOccuredWhenWritingFile_ErrorIsLoggedAndReturnsNull() - { - file.ReadAllText(GetFilePath("projectKey")).Throws(new Exception("Test Exception")); - - var actual = testSubject.Get("projectKey"); - - logger.AssertOutputStrings("[Roslyn Suppressions] Error loading settings for project projectKey. Issues suppressed on the server will not be suppressed in the IDE. Error: Test Exception"); - actual.Should().BeNull(); - } - - [TestMethod] - public void Update_HasNoIssues_FileWritten() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = Enumerable.Empty() }; - - testSubject.Update(settings, "mySolution1"); - - CheckFileWritten(settings, "mySolution1"); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Get_HasNoIssues_ReturnsEmpty() - { - var settings = new RoslynSettings { SonarProjectKey = "projectKey", Suppressions = Enumerable.Empty() }; - MockFileReadAllText(settings); - - var actual = testSubject.Get("projectKey"); - - var issuesGotten = actual.Suppressions.ToList(); - file.Received(1).ReadAllText(GetFilePath("projectKey")); - logger.AssertNoOutputMessages(); - issuesGotten.Count.Should().Be(0); - } - - [TestMethod] - public void Get_FileDoesNotExist_ErrorIsLoggedAndReturnsNull() - { - MockFileSystem(false); - - var actual = testSubject.Get("projectKey"); - - logger.AssertOutputStrings( - "[Roslyn Suppressions] Error loading settings for project projectKey. Issues suppressed on the server will not be suppressed in the IDE. Error: Settings File was not found"); - actual.Should().BeNull(); - } - - [TestMethod] // Regression test for SLVS-2946 - public void SaveAndLoadSettings() - { - string serializedText = null; - CreateSaveAndReloadFile(serializedText); - var projectKey = "projectKey"; - var original = new RoslynSettings - { - SonarProjectKey = projectKey, - Suppressions = - [ - CreateIssue("rule1", "path1", null), // null line number - CreateIssue("RULE2", "PATH2", 111, null, RoslynLanguage.VB) // null hash - ] - }; - - // Act - testSubject.Update(original, "any"); - - var reloaded = testSubject.Get(projectKey); - - reloaded.SonarProjectKey.Should().Be(projectKey); - reloaded.Suppressions.Should().NotBeNull(); - reloaded.Suppressions.Count().Should().Be(2); - - var firstSuppression = reloaded.Suppressions.First(); - firstSuppression.RoslynRuleId.Should().Be("rule1"); - firstSuppression.FilePath.Should().Be("path1"); - firstSuppression.RoslynIssueLine.Should().BeNull(); - firstSuppression.Hash.Should().Be("hash"); - firstSuppression.RoslynLanguage.Should().Be(RoslynLanguage.CSharp); - - var secondSuppression = reloaded.Suppressions.Last(); - secondSuppression.RoslynRuleId.Should().Be("RULE2"); - secondSuppression.FilePath.Should().Be("PATH2"); - secondSuppression.RoslynIssueLine.Should().Be(111); - secondSuppression.Hash.Should().BeNull(); - secondSuppression.RoslynLanguage.Should().Be(RoslynLanguage.VB); - } - - [TestMethod] - public void Delete_FileIsDeleted() - { - testSubject.Delete(SolutionName); - - file.Received(1).Delete(GetFilePath(SolutionName)); - logger.AssertNoOutputMessages(); - } - - [TestMethod] - public void Delete_DeletionFails_Logs() - { - var errorMessage = "deletion failed"; - fileSystem.File.When(x => x.Delete(GetFilePath(SolutionName))).Do(_ => throw new Exception(errorMessage)); - - testSubject.Delete(SolutionName); - - file.Received(1).Delete(GetFilePath(SolutionName)); - logger.AssertPartialOutputStrings(string.Format(Strings.RoslynSettingsFileStorageDeleteError, SolutionName, errorMessage)); - } - - [TestMethod] - public void Delete_CriticalException_ExceptionThrown() - { - fileSystem.File.When(x => x.Delete(GetFilePath(SolutionName))).Do(_ => throw new StackOverflowException()); - - Action act = () => testSubject.Delete(SolutionName); - - act.Should().Throw(); - } - - private void MockFileSystem(bool fileExists = true) - { - file.Exists(Arg.Any()).Returns(fileExists); - fileSystem.File.Returns(file); - - var directoryObject = Substitute.For(); - fileSystem.Directory.Returns(directoryObject); - } - - private void CheckFileWritten(RoslynSettings settings, string solutionName) - { - var expectedFilePath = GetFilePath(solutionName); - var expectedContent = JsonConvert.SerializeObject(settings, Formatting.Indented); - - file.Received(1).WriteAllText(expectedFilePath, expectedContent); - } - - private static string GetFilePath(string projectKey) => RoslynSettingsFileInfo.GetSettingsFilePath(projectKey); - - private void MockFileReadAllText(RoslynSettings settings) => file.ReadAllText(GetFilePath(settings.SonarProjectKey)).Returns(JsonConvert.SerializeObject(settings)); - - private void CreateSaveAndReloadFile(string serializedText) - { - // "Save" the data that was written - file.When(x => x.WriteAllText(Arg.Any(), Arg.Any())) - .Do(callInfo => - { - serializedText = callInfo.ArgAt(1); - }); - - // "Load" the saved data - // Note: using a function here, so the method returns the value of serializedText when the - // method is called, rather than when the mock is created (which would always be null) - file.ReadAllText(Arg.Any()) - .Returns( - _ => - { - serializedText.Should().NotBeNull("Test error: data has not been saved"); - return serializedText; - }); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs deleted file mode 100644 index b451132f2f..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/RoslynSettingsFileWatcherTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.IO.Abstractions; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class RoslynSettingsFileWatcherTests - { - [TestMethod] - public void Ctor_RegisterToFileWatcherEvents() - { - var fileSystemWatcher = CreateFileSystemWatcher(); - - CreateTestSubject(fileSystemWatcher: fileSystemWatcher.Object); - - fileSystemWatcher.VerifyAdd(x => x.Created += It.IsAny(), Times.Once); - fileSystemWatcher.VerifyAdd(x => x.Deleted += It.IsAny(), Times.Once); - fileSystemWatcher.VerifyAdd(x => x.Changed += It.IsAny(), Times.Once); - fileSystemWatcher.VerifySet(x=> x.Filter = "*.json", Times.Once); - fileSystemWatcher.VerifySet(x=> x.EnableRaisingEvents = true, Times.Once); - - fileSystemWatcher.VerifyNoOtherCalls(); - } - - [TestMethod] - public void Dispose_UnregisterFromFileWatcherEvents() - { - var fileSystemWatcher = CreateFileSystemWatcher(); - - var testSubject = CreateTestSubject(fileSystemWatcher: fileSystemWatcher.Object); - - fileSystemWatcher.VerifyRemove(x => x.Created -= It.IsAny(), Times.Never); - fileSystemWatcher.VerifyRemove(x => x.Deleted -= It.IsAny(), Times.Never); - fileSystemWatcher.VerifyRemove(x => x.Changed -= It.IsAny(), Times.Never); - fileSystemWatcher.Verify(x => x.Dispose(), Times.Never); - - testSubject.Dispose(); - - fileSystemWatcher.VerifyRemove(x => x.Created -= It.IsAny(), Times.Once); - fileSystemWatcher.VerifyRemove(x => x.Deleted -= It.IsAny(), Times.Once); - fileSystemWatcher.VerifyRemove(x => x.Changed -= It.IsAny(), Times.Once); - fileSystemWatcher.Verify(x=> x.Dispose(), Times.Once); - } - - [TestMethod] - [Description("It is a deliberate design choice to let the caller handle initialization exceptions.")] - public void Ctor_FailedToCreateFileSystemWatcher_ExceptionsNotHandled() - { - var fileSystemWatcher = new Mock(); - fileSystemWatcher - .SetupSet(x => x.EnableRaisingEvents = true) - .Throws(new NotImplementedException("some exception")); - - Action act = () => CreateTestSubject(fileSystemWatcher: fileSystemWatcher.Object); - - act.Should().Throw().And.Message.Should().Be("some exception"); - } - - [TestMethod] - [DataRow(WatcherChangeTypes.Created)] - [DataRow(WatcherChangeTypes.Deleted)] - [DataRow(WatcherChangeTypes.Changed)] - public void OnFileSystemChanges_SettingsKeyNotFound_CacheNotInvalidated(WatcherChangeTypes changeType) - { - var fileSystemWatcher = new Mock(); - var settingsCache = new Mock(); - - CreateTestSubject(settingsCache.Object, fileSystemWatcher.Object); - - settingsCache.Invocations.Count.Should().Be(0); - - RaiseFileSystemEvent(fileSystemWatcher, changeType, null); - - settingsCache.Invocations.Count.Should().Be(0); - } - - [TestMethod] - [DataRow(WatcherChangeTypes.Created)] - [DataRow(WatcherChangeTypes.Deleted)] - [DataRow(WatcherChangeTypes.Changed)] - public void OnFileSystemChanges_CacheInvalidated(WatcherChangeTypes changeType) - { - var fileSystemWatcher = new Mock(); - var settingsCache = new Mock(); - - CreateTestSubject(settingsCache.Object, fileSystemWatcher.Object); - - settingsCache.Invocations.Count.Should().Be(0); - - RaiseFileSystemEvent(fileSystemWatcher, changeType, "c:\\a\\b\\c\\some file.txt"); - - settingsCache.Verify(x=> x.Invalidate("some file"), Times.Once); - settingsCache.VerifyNoOtherCalls(); - } - - [TestMethod] - [DataRow(WatcherChangeTypes.Created)] - [DataRow(WatcherChangeTypes.Deleted)] - [DataRow(WatcherChangeTypes.Changed)] - public void OnFileSystemChanges_ExceptionInvalidatingCache_ExceptionHandled(WatcherChangeTypes changeType) - { - var fileSystemWatcher = new Mock(); - - var settingsCache = new Mock(); - settingsCache - .Setup(x => x.Invalidate("some file")) - .Throws(new NotImplementedException("some exception")); - - var logger = new TestLogger(); - - CreateTestSubject(settingsCache.Object, fileSystemWatcher.Object, logger: logger); - - settingsCache.Invocations.Count.Should().Be(0); - - Action act = () => RaiseFileSystemEvent(fileSystemWatcher, changeType, "c:\\a\\b\\c\\some file.txt"); - - act.Should().NotThrow(); - logger.AssertPartialOutputStringExists("some exception"); - } - - private static Mock CreateFileSystemWatcher() - { - var fileSystemWatcher = new Mock(); - - fileSystemWatcher.SetupAdd(x => x.Created += null); - fileSystemWatcher.SetupAdd(x => x.Deleted += null); - fileSystemWatcher.SetupAdd(x => x.Changed += null); - - fileSystemWatcher.SetupRemove(x => x.Created -= null); - fileSystemWatcher.SetupRemove(x => x.Deleted -= null); - fileSystemWatcher.SetupRemove(x => x.Changed -= null); - - return fileSystemWatcher; - } - - private static void RaiseFileSystemEvent(Mock fileSystemWatcher, WatcherChangeTypes changeType, string fileName) - { - var eventArgs = new FileSystemEventArgs(changeType, "some dir", fileName); - - switch (changeType) - { - case WatcherChangeTypes.Created: - fileSystemWatcher.Raise(x => x.Created += null, eventArgs); - break; - case WatcherChangeTypes.Deleted: - fileSystemWatcher.Raise(x => x.Deleted += null, eventArgs); - break; - case WatcherChangeTypes.Changed: - fileSystemWatcher.Raise(x => x.Changed += null, eventArgs); - break; - default: - throw new ArgumentOutOfRangeException(nameof(changeType), changeType, null); - } - } - - private static SuppressedIssuesFileWatcher CreateTestSubject(ISettingsCache settingsCache = null, - IFileSystemWatcher fileSystemWatcher = null, - IFileSystem fileSystem = null, - ILogger logger = null) - { - fileSystemWatcher ??= Mock.Of(); - fileSystem ??= Mock.Of(); - logger ??= Mock.Of(); - - Mock.Get(fileSystem) - .Setup(x => x.FileSystemWatcher.FromPath(RoslynSettingsFileInfo.Directory)) - .Returns(fileSystemWatcher); - - Mock.Get(fileSystem).Setup(x => x.Directory.CreateDirectory(RoslynSettingsFileInfo.Directory)); - - settingsCache ??= Mock.Of(); - - return new SuppressedIssuesFileWatcher(settingsCache, logger, fileSystem); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs deleted file mode 100644 index ea855af74c..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/Settings.Cache/SettingsCacheTest.cs +++ /dev/null @@ -1,165 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.TestInfrastructure.Helpers; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.Settings.Cache -{ - [TestClass] - public class SettingsCacheTest - { - [TestMethod] - public void GetSettings_SettingNotInCache_SettingsReadFromFile() - { - var settings = new RoslynSettings { SonarProjectKey = "my project" }; - var cacheObject = CreateEmptyCacheObject(); - - var fileStorage = new Mock(); - fileStorage.Setup(fs => fs.Get("settingsKey")).Returns(settings); - - var testSubject = CreateTestSubject(fileStorage, cacheObject); - var actual = testSubject.GetSettings("settingsKey"); - - fileStorage.Verify(fs => fs.Get("settingsKey"), Times.Once); - cacheObject.ContainsKey("settingsKey").Should().BeTrue(); - cacheObject["settingsKey"].Should().BeSameAs(settings); - actual.Should().BeSameAs(settings); - } - - [DataRow("settingskey", "settingskey")]//Testing lower case - [DataRow("settingsKEY", "settingskey")]//Testing upper case - [DataRow("III", "iii")]//Testing upper case with invariant culture - [TestMethod] - public void GetSettings_SettingInCache_SettingsReadFromCache(string settingsKey, string normalisedKey) - { - //This is to make sure normalising the keys done correctly with invariant culture - //https://en.wikipedia.org/wiki/Dotted_and_dotless_I - using var scope = new TurkishCultureScope(); - - var settings = new RoslynSettings { SonarProjectKey = "my project" }; - - var cacheObject = CreatePopulatedCacheObject(normalisedKey, settings); - - var fileStorage = new Mock(); - - var testSubject = CreateTestSubject(fileStorage, cacheObject); - var actual = testSubject.GetSettings(settingsKey); - - fileStorage.Verify(fs => fs.Get(It.IsAny()), Times.Never); - actual.Should().BeSameAs(settings); - - } - - [TestMethod] - public void GetSettings_SettingNotInCacheOrFile_SettingsEmpty() - { - var fileStorage = new Mock(); - - var testSubject = CreateTestSubject(fileStorage); - var actual = testSubject.GetSettings("settingsKey"); - - fileStorage.Verify(fs => fs.Get("settingsKey"), Times.Once); - CheckSettingsAreEmpty(actual); - } - - [TestMethod] - public void GetSettings_DifferentSettingInCache_SettingsEmpty() - { - var settings = new RoslynSettings { SonarProjectKey = "my project" }; - - var cacheObject = CreatePopulatedCacheObject("differentkey", settings); - - var fileStorage = new Mock(); - - var testSubject = CreateTestSubject(fileStorage, cacheObject); - var actual = testSubject.GetSettings("settingsKey"); - - fileStorage.Verify(fs => fs.Get("settingsKey"), Times.Once); - CheckSettingsAreEmpty(actual); - } - - [DataRow("settingskey", "settingskey")]//Testing lower case - [DataRow("settingsKEY", "settingskey")]//Testing upper case - [DataRow("III","iii")]//Testing upper case with invariant culture - [TestMethod] - public void Invalidate_SettingInCache_SettingsRemovedFromCache(string settingsKey, string normalisedKey) - { - //This is to make sure normalising the keys done correctly with invariant culture - //https://en.wikipedia.org/wiki/Dotted_and_dotless_I - using var scope = new TurkishCultureScope(); - - var cacheObject = CreatePopulatedCacheObject(normalisedKey, new RoslynSettings()); - cacheObject.ContainsKey(normalisedKey).Should().BeTrue("Test setup error: cache was not pre-populated correctly"); - - var testSubject = CreateTestSubject(settingsCollection: cacheObject); - testSubject.Invalidate(settingsKey); - - cacheObject.ContainsKey(normalisedKey).Should().BeFalse(); - - } - [TestMethod] - public void Invalidate_SettingNotInCache_NoErrorThrown() - { - var testSubject = CreateTestSubject(); - - testSubject.Invalidate("settingsKey"); - } - - private ConcurrentDictionary CreatePopulatedCacheObject(string settingsKey, RoslynSettings settings) - { - var cacheObject = CreateEmptyCacheObject(); - cacheObject.AddOrUpdate(settingsKey, settings, (x, y) => settings); - - return cacheObject; - } - - - private SettingsCache CreateTestSubject(Mock fileStorage = null, ConcurrentDictionary settingsCollection = null) - { - fileStorage = fileStorage ?? new Mock(); - settingsCollection = settingsCollection ?? new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - return new SettingsCache(fileStorage.Object, settingsCollection); - } - - private ConcurrentDictionary CreateEmptyCacheObject() - { - return new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - } - - private static void CheckSettingsAreEmpty(RoslynSettings settings) - { - settings.Should().BeSameAs(RoslynSettings.Empty); - - settings.SonarProjectKey.Should().BeNull(); - settings.Suppressions.Should().NotBeNull(); - settings.Suppressions.Count().Should().Be(0); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs deleted file mode 100644 index d1c377e21a..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SonarDiagnosticSuppressorTests.cs +++ /dev/null @@ -1,244 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Immutable; -using System.Linq; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SonarDiagnosticSuppressorTests - { - [TestMethod] - public void Suppressor_ReturnsExpectedSuppressions() - { - var testSubject = CreateTestSubject(); - - var actual = testSubject.SupportedSuppressions; - - // Note: ImmutableArray<> is a value type so we can't use ReferenceEquals or ".Should().BeSameAs(...)" - // to check if the object is the same. Howevere, "Equals" is overloaded to test the underlying arrays are - // the same instance. - // See https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablearray-1.equals?view=net-6.0 - actual.Equals(SupportedSuppressionsBuilder.Instance.Descriptors).Should().BeTrue(); - } - - [TestMethod] - public void Suppressors_AllSuppressorsReturnSameInstances() - { - // Perf - check every instance reuses the same set of descriptors - var supported1 = CreateTestSubject().SupportedSuppressions; - var supported2 = CreateTestSubject().SupportedSuppressions; - - // Have to use "Equals" to test identityt equality - see above test. - supported1.Equals(supported2).Should().BeTrue(); - } - - [TestMethod] - public void GetSuppressionsIsNotCalled_ContainerNotInitialized() - { - var createContainer = new Mock>(); - - var testSubject = CreateTestSubject(createContainer.Object); - - createContainer.Invocations.Count.Should().Be(0); - - var supportedSuppressions = testSubject.SupportedSuppressions; - - supportedSuppressions.Should().NotBeEmpty(); - - createContainer.Invocations.Count.Should().Be(0); - } - - [TestMethod] - public void GetSuppressions_IsNotInConnectedMode_ContainerNotInitialized() - { - var createContainer = new Mock>(); - - var suppressionContext = CreateSuppressionContext(null); - var reportedDiagnostics = CreateReportedDiagnostics(); - - var testSubject = CreateTestSubject(createContainer.Object); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object); - - suppressions.Count().Should().Be(0); - - createContainer.Invocations.Count.Should().Be(0); - - suppressionContext.VerifyGet(c => c.IsInConnectedMode, Times.Once); - } - - [TestMethod] - public void GetSuppressions_IsInConnectedMode_ContainerInitialized() - { - var suppressionContext = CreateSuppressionContext("sonarKey"); - var suppressionChecker = CreateSuppressionChecker("sonarKey"); - - var createContainer = new Mock>(); - createContainer.Setup(x => x()).Returns(CreateContainer(suppressionChecker)); - - var reportedDiagnostics = CreateReportedDiagnostics(); - - var testSubject = CreateTestSubject(createContainer.Object); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object); - - suppressions.Count().Should().Be(0); - - createContainer.Invocations.Count.Should().Be(1); - } - - [TestMethod] - public void GetSuppressions_IsNotInConnectedMode_ShouldReturnEmpty() - { - var suppressionContext = CreateSuppressionContext(null); - var reportedDiagnostics = CreateReportedDiagnostics(); - - var testSubject = CreateTestSubject(); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object); - - suppressions.Count().Should().Be(0); - } - - [TestMethod] - public void GetSuppressions_HasSuppressedDiagnostic_ShouldReturnSuppressions() - { - var suppressionContext = CreateSuppressionContext("sonarKey"); - var diag = CreateDiagnostic("S100"); - - var suppressionChecker = CreateSuppressionChecker("sonarKey", diag); - var container = CreateContainer(suppressionChecker); - - var reportedDiagnostics = CreateReportedDiagnostics(diag); - - var testSubject = CreateTestSubject(container); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object).ToList(); - - suppressions.Count().Should().Be(1); - suppressions[0].Descriptor.SuppressedDiagnosticId.Should().Be("S100"); - } - - [TestMethod] - public void GetSuppressions_HasMixedSuppressedDiagnostic_ShouldReturnOnlySuppressed() - { - - var suppressionContext = CreateSuppressionContext("sonarKey"); - var diag1 = CreateDiagnostic("S100"); - var diag2 = CreateDiagnostic("S101"); - var diag3 = CreateDiagnostic("S103"); - - var suppressionChecker = CreateSuppressionChecker("sonarKey", diag1, diag2); - var container = CreateContainer(suppressionChecker); - - var reportedDiagnostics = CreateReportedDiagnostics(diag1, diag2, diag3); - - var testSubject = CreateTestSubject(container); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object).ToList(); - - suppressions.Count().Should().Be(2); - suppressions[0].Descriptor.SuppressedDiagnosticId.Should().Be("S100"); - suppressions[1].Descriptor.SuppressedDiagnosticId.Should().Be("S101"); - } - - [TestMethod] - public void GetSuppressions_HasNotSuppressedDiagnostic_ShouldReturnEmpty() - { - var suppressionContext = CreateSuppressionContext("sonarKey"); - var diag = CreateDiagnostic("S100"); - - var reportedDiagnostics = CreateReportedDiagnostics(diag); - - var testSubject = CreateTestSubject(); - - var suppressions = testSubject.GetSuppressions(reportedDiagnostics, suppressionContext.Object).ToList(); - - suppressions.Count().Should().Be(0); - } - - private ISuppressionChecker CreateSuppressionChecker(string settingsKey, params Diagnostic[] diagnostics) - { - var suppressionChecker = new Mock(); - suppressionChecker.Setup(sc => sc.IsSuppressed(It.IsAny(), It.IsAny())).Returns(false); - - foreach (var diagnostic in diagnostics) - { - suppressionChecker.Setup(sc => sc.IsSuppressed(diagnostic, settingsKey)).Returns(true); - } - - return suppressionChecker.Object; - } - - private Mock CreateSuppressionContext(string settingsKey) - { - var context = new Mock(); - context.SetupGet(c => c.SettingsKey).Returns(settingsKey); - context.SetupGet(c => c.IsInConnectedMode).Returns(settingsKey != null); - - return context; - } - - private Diagnostic CreateDiagnostic(string id) - { - var diagnostic = new Mock(); - diagnostic.SetupGet(d => d.Id).Returns(id); - - return diagnostic.Object; - } - - private ImmutableArray CreateReportedDiagnostics(params Diagnostic[] diagnostics) - { - return diagnostics.ToImmutableArray(); - } - - private SonarDiagnosticSuppressor CreateTestSubject(IContainer container) => - CreateTestSubject(() => container); - - private SonarDiagnosticSuppressor CreateTestSubject(Func createContainer = null) - { - createContainer ??= () => CreateContainer(); - - return new SonarDiagnosticSuppressor(createContainer); - } - - private IContainer CreateContainer(ISuppressionChecker suppressionChecker = null, ILogger logger = null) - { - suppressionChecker ??= Mock.Of(); - logger ??= Mock.Of(); - - var container = new Mock(); - container.SetupGet(c => c.SuppressionChecker).Returns(suppressionChecker); - container.SetupGet(c => c.Logger).Returns(logger); - - return container.Object; - } - - - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs deleted file mode 100644 index c8ca6d36f2..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SupportedSuppressionBuilderTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SupportedSuppressionBuilderTests - { - [TestMethod] - public void SupportedSuppressors_Instance_IsSingleton() - { - SupportedSuppressionsBuilder.Instance.Should().BeSameAs(SupportedSuppressionsBuilder.Instance); - } - - [TestMethod] - public void SupportedSuppressors_SuppressorsCreated() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - // The exact number will vary every time a new version of either C# or VB analyzer is released - // so we'll check a ball-park figure - result.Count().Should().BeGreaterThan(400); - } - - [TestMethod] - public void SupportedSuppressors_ItemsAreNotNull() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - result.Any(x => x == null).Should().BeFalse(); - } - - [TestMethod] - public void SupportedSuppressions_NoDuplicates() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - var distinctIdCount = result.Select(x => x.Id).Distinct().Count(); - result.Length.Should().Be(distinctIdCount); - } - - [TestMethod] - public void SupportedSuppressors_NoUtilityAnalyzers() - { - var result = SupportedSuppressionsBuilder.Instance.Descriptors; - - result.Any(x => x.Id.StartsWith("S9999")).Should().BeFalse(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs deleted file mode 100644 index 2e3dd89712..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressedIssueTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests; - -[TestClass] -public class SuppressedIssueTests -{ - [TestMethod] - public void AreSame_ReturnsTrueIfPropertiesHaveSameValue() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeTrue(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfRuleIdIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(ruleId: "S666"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfFilePathIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(path: "differentPath"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfLineIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(line: suppressedIssue1.RoslynIssueLine + 1); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfHashIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(hash: "differentHash"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfLanguageIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(language: RoslynLanguage.VB); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfIssueServerKeyIsDifferent() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - var suppressedIssue2 = TestHelper.CreateIssue(issueServerKey: "key6"); - - suppressedIssue1.AreSame(suppressedIssue2).Should().BeFalse(); - } - - [TestMethod] - public void AreSame_ReturnsFalseIfSuppressedIssueIsNull() - { - var suppressedIssue1 = TestHelper.CreateIssue(); - - suppressedIssue1.AreSame(null).Should().BeFalse(); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs deleted file mode 100644 index 21e6a3d82c..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionCheckerTests.cs +++ /dev/null @@ -1,401 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.IO; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarQube.Client; -using static SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests.TestHelper; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SuppressionCheckerTests - { - // Cache that will throw if it is called - private static readonly ISettingsCache ThrowingSettingsCache = new Mock(MockBehavior.Strict).Object; - - private readonly Diagnostic ValidInSourceDiagnostic = CreateDiagnostic("any", - CreateSourceFileLocation("c:\\any.txt")); - - [TestMethod] - public void IsSuppressed_NonSourceLocation_ReturnsFalse() - { - // Perf - should early-out for non-source location - var testSubject = CreateTestSubject(ThrowingSettingsCache); - - var nonSourceDiagnostic = CreateDiagnostic("any", CreateNonSourceLocation()); - - // Act - var actual = testSubject.IsSuppressed(nonSourceDiagnostic, "any"); - - actual.Should().BeFalse(); - } - - [TestMethod] - public void IsSuppressed_NullDiagnosticLocation_ReturnsFalse() - { - // Perf - should early-out for null diagnostic location - var testSubject = CreateTestSubject(ThrowingSettingsCache); - - var nullLocationDiagnostic = CreateDiagnostic("any", null); - - // Act - var actual = testSubject.IsSuppressed(nullLocationDiagnostic, "any"); - - actual.Should().BeFalse(); - } - - - [TestMethod] - public void IsSuppressed_NullSettings_ReturnsFalse() - { - var cache = CreateSettingsCache("settingsKey1", null); - var testSubject = CreateTestSubject(cache.Object); - - // Act - var actual = testSubject.IsSuppressed(ValidInSourceDiagnostic, "settingsKey1"); - - actual.Should().Be(false); - CheckGetSettingsCalled(cache, "settingsKey1"); - } - - [TestMethod] - public void IsSuppressed_EmptySettings_ReturnsFalse() - { - var cache = CreateSettingsCache("settingsKey1", new SuppressedIssue[0]); - - var testSubject = CreateTestSubject(cache.Object); - - // Act - var actual = testSubject.IsSuppressed(ValidInSourceDiagnostic, "settingsKey1"); - - actual.Should().Be(false); - CheckGetSettingsCalled(cache, "settingsKey1"); - } - - [TestMethod] - public void IsSuppressed_HasIssues_NoMatches_ReturnsFalse() - { - var diagnostic = CreateDiagnostic("S111", CreateSourceFileLocation("c:\\myfile.cs")); - - var suppressedIssues = new SuppressedIssue[] - { - CreateIssue("S111", "wrongFile1.txt") - }; - - var cache = CreateSettingsCache("settingsKey", suppressedIssues); - var checksumCalculator = CreateChecksumCalculator("any"); - var testSubject = CreateTestSubject(cache.Object, checksumCalculator.Object); - - // Act - var actual = testSubject.IsSuppressed(diagnostic, "settingsKey"); - - actual.Should().BeFalse(); - CheckGetSettingsCalled(cache, "settingsKey"); - } - - [TestMethod] - public void IsSuppressed_HasIssues_Matches_ReturnsTrue() - { - var location = CreateSourceFileLocation(DiagFileName, DiagFileText, DiagSelectedText); - var diagnostic = CreateDiagnostic(DiagRuleId, location); - - var suppressedIssues = new[] - { - CreateIssueFromDiagnostic(diagnostic) - }; - - var checksumCalculator = CreateChecksumCalculator(DiagHash); - - var cache = CreateSettingsCache("settingsKey", suppressedIssues); - var testSubject = CreateTestSubject(cache.Object, checksumCalculator.Object); - - // Act - var actual = testSubject.IsSuppressed(diagnostic, "settingsKey"); - - actual.Should().BeTrue(); - CheckGetSettingsCalled(cache, "settingsKey"); - } - - [TestMethod] - public void IsMatch_FilesAreDifferent_ReturnsFalse() - { - var checksumCalculator = CreateChecksumCalculator("hash"); - - var diag = CreateDiagnostic("S999", CreateSourceFileLocation("c:\\diagnosticFileName.cs", "all text", "all")); - // Sanity check the diagnostic location is on the expected line - diag.Location.GetLineSpan().StartLinePosition.Line.Should().Be(0, "Test setup error"); - - var issue = CreateIssue("S999", "issueFile.cs", 1, "hash"); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().BeFalse(); - - checksumCalculator.Invocations.Count.Should().Be(0); - } - - [TestMethod] - public void IsMatch_RuleIdsAreDifferent_ReturnsFalse() - { - var checksumCalculator = CreateChecksumCalculator("hash"); - - var diag = CreateDiagnostic("S111", CreateSourceFileLocation("c:\\file.cs", "all text", "all")); - // Sanity check the diagnostic location is on the expected line - diag.Location.GetLineSpan().StartLinePosition.Line.Should().Be(0, "Test setup error"); - - var issue = CreateIssue("S999", "c\\file.cs", 1, "hash"); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().BeFalse(); - - checksumCalculator.Invocations.Count.Should().Be(0); - - } - - #region Matching tests - - // Constants used in for the diagnostic to match against in IsMatch_ReturnsExpectedValue - // They are defined as constants here so we can use them in the [DataRow]s for the test. - private const string DiagFileName = "c:\\diag.txt"; - private const string MatchingServerFileName = "diag.txt"; - private const string DiagRuleId = "S999"; - private const string DiagHash = "diag hash"; - - private const string DiagFileText = "0\n1\n2\n3 text \n\n"; - private const string DiagSelectedText = "text"; - private const int DiagRoslynLineNumber = 3; // the 0-base line in which the word "text" appears - - [TestMethod] - [DataRow(MatchingServerFileName, DiagRuleId, DiagRoslynLineNumber, DiagHash, true)] - [DataRow("wrong file name.cs", DiagRuleId, DiagRoslynLineNumber, DiagHash, false)] - [DataRow(MatchingServerFileName, "wrong rule id", DiagRoslynLineNumber, DiagHash, false)] - [DataRow(MatchingServerFileName, DiagRuleId, 999, "wrong hash", false)] // wrong line, wrong hash -> false - [DataRow(MatchingServerFileName, DiagRuleId, DiagRoslynLineNumber, "wrong hash", true)] // right line, wrong hash -> true - [DataRow(MatchingServerFileName, DiagRuleId, 888, DiagHash, true)] // wrong line, right hash -> true - - // Special cases - [DataRow("DIAG.TXT", DiagRuleId, DiagRoslynLineNumber, DiagHash, true)] // case-insensitive - [DataRow(MatchingServerFileName, "s999", DiagRoslynLineNumber, DiagHash, true)] // case-insensitive - public void IsMatch_ReturnsExpectedValue(string issueFile, string issueRuleId, int issueLine, - string issueHash, bool expected) - { - // The diagnostic is the same in every case - var checksumCalculator = CreateChecksumCalculator(DiagHash); - var diag = CreateDiagnostic(DiagRuleId, CreateSourceFileLocation(DiagFileName, DiagFileText, DiagSelectedText)); - // Sanity check the diagnostic location is on the expected line - diag.Location.GetLineSpan().StartLinePosition.Line.Should().Be(DiagRoslynLineNumber, "Test setup error"); - - var issue = CreateIssue(issueRuleId, issueFile, issueLine, issueHash); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().Be(expected); - } - - [TestMethod] - [DataRow(0, 0, true)] - [DataRow(1, 0, false)] - [DataRow(1, 1, false)] - [DataRow(2, 0, false)] - [DataRow(2, 2, false)] - public void IsMatch_IsRoslynFileLevelIssue_MatchesSonarFileLevelIssue(int roslynStartPosition, int length, - bool expected) - { - const string text = "000\n111\n222\n"; - var selectedSpan = new TextSpan(roslynStartPosition, length); - var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(text, path: MatchingWellKnownLocalPath); - var location = Location.Create(syntaxTree, selectedSpan); - var diagnostic = CreateDiagnostic("id", location); - - var sonarFileLevelIssue = CreateIssue(ruleId: "id", path: WellKnownRelativeServerPath, hash: "hash", line: null); - - var actual = SuppressionChecker.IsMatch(diagnostic, sonarFileLevelIssue, Mock.Of()); - - actual.Should().Be(expected); - } - - [TestMethod] - public void IsMatch_RoslynFileLevelIssue_SonarNonFileLevelIssue_DoNotMatch() - { - const string text = "000\n111\n222\n"; - var selectedSpan = new TextSpan(0, 0); - var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(text, path: MatchingWellKnownLocalPath); - var location = Location.Create(syntaxTree, selectedSpan); - var diagnostic = CreateDiagnostic("id", location); - - var sonarNonFileLevelIssue = CreateIssue(ruleId: "id", path: WellKnownRelativeServerPath, hash: "hash", line: 2); - - var actual = SuppressionChecker.IsMatch(diagnostic, sonarNonFileLevelIssue, Mock.Of()); - - actual.Should().BeFalse(); - } - - private const string WellKnownRelativeServerPath = "file.txt"; - private const string MatchingWellKnownLocalPath = "c:\\" + WellKnownRelativeServerPath; - - [TestMethod] - [DataRow(@"same.txt", @"c:\same.txt", true)] - [DataRow(@"SAME.TXT", @"c:\same.txt", true)] - [DataRow(@"differentExt.123", @"d:\differentExt.999", false)] - [DataRow(@"partial\file.cs", @"c:\aaa\partial\file.cs", true)] - [DataRow(@"aaa\partial\file.cs", @"x:\partial\file.cs", false)] - public void IsMatch_IsSameFile(string serverIssueFile, string diagFile, bool expected) - { - var checksumCalculator = CreateChecksumCalculator("a hash that won't match"); - - var diag = CreateDiagnostic(DiagRuleId, CreateSourceFileLocation(diagFile, DiagFileText, DiagSelectedText)); - - var issue = CreateIssueFromDiagnostic(diag, overrideFilePath: serverIssueFile); - - SuppressionChecker.IsMatch(diag, issue, checksumCalculator.Object) - .Should().Be(expected); - } - - #endregion // Matching tests - - private void CheckGetSettingsCalled(Mock cache, string expectedSettingsKey) => - cache.Verify(x => x.GetSettings(expectedSettingsKey), Times.Once()); - - private static SuppressionChecker CreateTestSubject(ISettingsCache cache = null, - IChecksumCalculator checksumCalculator = null) - { - cache ??= CreateSettingsCache("any", null).Object; - checksumCalculator ??= CreateChecksumCalculator("any").Object; - - return new SuppressionChecker(cache, checksumCalculator); - } - - private static Mock CreateSettingsCache(string settingsKey, IEnumerable suppressedIssues) - { - var cache = new Mock(); - var settings = new RoslynSettings { Suppressions = suppressedIssues }; - cache.Setup(x => x.GetSettings(settingsKey)).Returns(settings); - return cache; - } - - private static Mock CreateChecksumCalculator(string hashToReturn) - { - var calculator = new Mock(); - calculator.Setup(x => x.Calculate(It.IsAny())) - .Returns(hashToReturn); - - return calculator; - } - - /// - /// Returns an issue with the same values as the supplied diagnostic. - /// - /// - /// The issue will match the diagnostic, unless the file path is overridden. - /// The diagnostic must have a valid location and source tree. - /// - private static SuppressedIssue CreateIssueFromDiagnostic(Diagnostic diagnostic, - string overrideFilePath = null) - { - // The issue will match because the line number is the same (so the hash is irrelevant) - var sonarLine = diagnostic.Location.GetLineSpan().EndLinePosition.Line; - - var serverIssuePath = overrideFilePath ?? GetMatchingRelativePath(diagnostic.Location.SourceTree.FilePath); - - var issueHash = Guid.NewGuid().ToString(); - - return CreateIssue(diagnostic.Id, serverIssuePath, sonarLine, issueHash); - } - - private static string GetMatchingRelativePath(string absolutePath) - { - // The matching server should not be rooted, so we'll strip off the - // root and return the rest of the path. The product code should be - // considered that to be a valid match for the original full path. - if (!Path.IsPathRooted(absolutePath)) - { - throw new ArgumentException(nameof(absolutePath), $"Test setup error: path should be rooted. Actual: {absolutePath}"); - } - var root = Path.GetPathRoot(absolutePath); - return absolutePath.Substring(root.Length); - } - - #region Diagnostic helper methods - - private static Location CreateNonSourceLocation() - { - var nonSourceLocation = Location.Create("dummyFilePath.cs", new TextSpan(), new LinePositionSpan()); - nonSourceLocation.IsInSource.Should().BeFalse(); - nonSourceLocation.Kind.Should().NotBe(LocationKind.SourceFile); - return nonSourceLocation; - } - - /// - /// Returns a Location that is in a source file, backed by a valid syntax tree. - /// - /// The contents of the entire file (optional) - /// The specific text in the file the location points to (optional) - private static Location CreateSourceFileLocation(string filePath, - string fileText = "any", string selectedText = "any") - { - // Mocking source location so that it appears to be backed by a real syntax tree, with - // the required "GetLineSpan()", "GetText().Lines" etc methods working correctly is hard. - - // The simplest thing to do is create a real C# syntax tree. It doesn't matter if the - // contents of the fake file are not valid C# - the parser will still produce a valid - // document. - - var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(fileText, path: filePath); - var selectedSpan = CreateSpan(fileText, selectedText); - var location = Location.Create(syntaxTree, selectedSpan); - - // Sanity checks that the objects are set up as we expected - location.IsInSource.Should().BeTrue("Test setup error: location should be in source"); - location.Kind.Should().Be(LocationKind.SourceFile, "Test setup error: LocationKind should be SourceFile"); - syntaxTree.FilePath.Should().Be(filePath, "Test setup error: syntaxTree FilePath is not set correctly"); - var lineSpan = location.GetLineSpan(); - lineSpan.IsValid.Should().BeTrue("Test setup error: lineSpan is not valid"); - var lineText = syntaxTree.GetText().Lines[lineSpan.EndLinePosition.Line].ToString(); - lineText.Should().Contain(selectedText, "Test setup error: the fake location/syntax tree is not correctly constructed"); - - return location; - } - - private static TextSpan CreateSpan(string documentText, string selectedText) - { - var start = documentText.IndexOf(selectedText); - start.Should().BeGreaterThan(-1, "Test setup error: selected text is not in the document text"); - return new TextSpan(start, selectedText.Length); - } - - private static Diagnostic CreateDiagnostic(string ruleId, Location loc) - { - var diagnostic = new Mock(); - - diagnostic.Setup(x => x.Id).Returns(ruleId); - diagnostic.Setup(x => x.Location).Returns(loc); - - return diagnostic.Object; - } - - #endregion - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs deleted file mode 100644 index 9571aaa649..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/SuppressionExecutionContextTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Immutable; -using System.Threading; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - [TestClass] - public class SuppressionExecutionContextTests - { - [TestMethod] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName1\CSharp\SonarLint.xml", "solutionName1")] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName2\VB\SonarLint.xml", "solutionName2")] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName3\VB\sonarlint.xml", "solutionName3")] - [DataRow(@"C:\project\SonarLint for Visual Studio\Bindings\solutionName4\VB\SONARLINT.xml", "solutionName4")] - public void SonarProjectKey_PathIsValid_ReturnsExpectedSolutionName(string path, string solutionName) - { - var additionalText = new ConcreteAdditionalText(path); - - var testSubject = CreateTestSubject(additionalText); - - testSubject.SettingsKey.Should().Be(solutionName); - testSubject.IsInConnectedMode.Should().BeTrue(); - testSubject.Mode.Should().Be("Connected"); - } - - [TestMethod] - public void SonarProjectKey_PathIsNotValid_ReturnsNull() - { - var additionalText = new ConcreteAdditionalText(@"C:\project\projectKey1\CSharp\SonarLint.xml"); - - var testSubject = CreateTestSubject(additionalText); - - testSubject.SettingsKey.Should().BeNull(); - testSubject.IsInConnectedMode.Should().BeFalse(); - testSubject.Mode.Should().Be("Standalone"); - } - - [TestMethod] - public void SonarProjectKey_HasMixedPaths_ReturnsExpectedSolutionName() - { - var additionalText1 = new ConcreteAdditionalText(@"C:\project\projectKey1\CSharp\SonarLint.xml"); - var additionalText2 = new ConcreteAdditionalText(@"C:\project\SonarLint for Visual Studio\Bindings\expectedSlnName\CSharp\SonarLint.xml"); - - var testSubject = CreateTestSubject(additionalText1, additionalText2); - - testSubject.SettingsKey.Should().Be("expectedSlnName"); - testSubject.IsInConnectedMode.Should().BeTrue(); - testSubject.Mode.Should().Be("Connected"); - } - - [TestMethod] - public void SonarProjectKey_HasNoPaths_ReturnsNull() - { - var testSubject = CreateTestSubject(); - - testSubject.SettingsKey.Should().BeNull(); - testSubject.IsInConnectedMode.Should().BeFalse(); - testSubject.Mode.Should().Be("Standalone"); - } - - private SuppressionExecutionContext CreateTestSubject(params AdditionalText[] existingAdditionalFiles) - { - var analyzerOptions = CreateAnalyzerOptions(existingAdditionalFiles); - return new SuppressionExecutionContext(analyzerOptions); - } - - private AnalyzerOptions CreateAnalyzerOptions(params AdditionalText[] existingAdditionalFiles) - { - return new AnalyzerOptions(existingAdditionalFiles.ToImmutableArray()); - } - - } - - internal class ConcreteAdditionalText : AdditionalText - { - internal ConcreteAdditionalText(string path) - { - Path = path; - } - - public override string Path { get; } - - public override SourceText GetText(CancellationToken cancellationToken = default) - { - return SourceText.From(string.Empty); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs deleted file mode 100644 index adced96222..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/TestHelper.cs +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests -{ - internal static class TestHelper - { - public static SuppressedIssue CreateIssue( - string ruleId = "ruleId", - string path = "path", - int? line = 0, - string hash = "hash", - RoslynLanguage language = RoslynLanguage.CSharp, - string issueServerKey = "key") => - new SuppressedIssue - { - FilePath = path, - Hash = hash, - RoslynLanguage = language, - RoslynRuleId = ruleId, - RoslynIssueLine = line, - IssueServerKey = issueServerKey - }; - - public static SonarQubeIssue CreateSonarQubeIssue( - string ruleId = "any", - int? line = null, - string filePath = "filePath", - string hash = "hash", - bool isSuppressed = true, - string issueKey = null) - { - var sonarQubeIssue = new SonarQubeIssue( - issueKey ?? Guid.NewGuid().ToString(), - filePath, - hash, - "message", - "moduleKey", - ruleId, - false, // isResolved - SonarQubeIssueSeverity.Info, - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, - line.HasValue ? new IssueTextRange(line.Value, line.Value, 1, 999) : null, - null - ); - - sonarQubeIssue.IsResolved = isSuppressed; - - return sonarQubeIssue; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json b/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json deleted file mode 100644 index 53977d1d7c..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions.UnitTests/packages.lock.json +++ /dev/null @@ -1,1377 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v4.8": { - "FluentAssertions": { - "type": "Direct", - "requested": "[5.9.0, )", - "resolved": "5.9.0", - "contentHash": "JZxb5DuspmuH4A5p9bfj060evQnwIUkxxfFMG9F/pa7gl1Ry2r2nuQbu5G4UWGQQTtpTxPpVowqNDdBkA0G2Mw==" - }, - "FluentAssertions.Analyzers": { - "type": "Direct", - "requested": "[0.11.4, )", - "resolved": "0.11.4", - "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.2", - "System.Collections.Immutable": "5.0.0", - "System.Memory": "4.5.4", - "System.Reflection.Metadata": "5.0.0", - "System.Runtime.CompilerServices.Unsafe": "5.0.0", - "System.Text.Encoding.CodePages": "4.5.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "aDRRb7y/sXoJyDqFEQ3Il9jZxyUMHkShzZeCRjQf3SS84n2J0cTEi3TbwVZE9XJvAeMJhGfVVxwOdjYBg6ljmw==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[3.11.0]" - } - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[16.6.1, )", - "resolved": "16.6.1", - "contentHash": "zYAjfWzpxKb64P9ntReT1Xr8HdONZnpLVs12HIjXWo+UOCDpevP1UWRoaAgNysaD1/l3teBKvgbSeG9bRssfOQ==", - "dependencies": { - "Microsoft.CodeCoverage": "16.6.1" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.18.2, )", - "resolved": "4.18.2", - "contentHash": "SjxKYS5nX6prcaT8ZjbkONh3vnh0Rxru09+gQ1a07v4TM530Oe/jq3Q4dOZPfo1wq0LYmTgLOZKrqRfEx4auPw==", - "dependencies": { - "Castle.Core": "5.1.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "MSTest.TestAdapter": { - "type": "Direct", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "t2/rL9DG+cVAgPs98OGm2sbZ4FTgn+MGEan5P9NRAgqMV3+nYRKG7/5R0jY7lBMq9ISms+84MSqTHWs6QnPt4A==" - }, - "MSTest.TestFramework": { - "type": "Direct", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "kV/yZ0XLiOElsVeLT0GnNrsoKcPvVNOP6Cv2zkAiceJY0rpro0L+3t54bRApLwTg1mxlo4rLziBG7X6X69KcrQ==" - }, - "NSubstitute": { - "type": "Direct", - "requested": "[5.1.0, )", - "resolved": "5.1.0", - "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", - "dependencies": { - "Castle.Core": "5.1.1", - "System.Threading.Tasks.Extensions": "4.3.0" - } - }, - "NSubstitute.Analyzers.CSharp": { - "type": "Direct", - "requested": "[1.0.17, )", - "resolved": "1.0.17", - "contentHash": "Pwz0MD7CAM/G/fvJjM3ceOfI+S0IgjanHcK7evwyrW9qAWUG8fgiEXYfSX1/s3h2JUNDOw6ik0G8zp+RT61Y1g==" - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.4.0", - "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" - }, - "envdte": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "AC6jYeSnDnYZEs5nHKEtBupRWAQxriX2X3M25HyJlU9cvCCqPCByMwIbvlz8kXk+GfGxSL8sN+YOihU2SvrjXw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte100": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "XICXfPHF4SQzqpUtQgXZsuSTyYOdSOymPMUqH/Q6QBrpEoMiJxmW/eEvLwgqJqiW5+HaajJImlVJkrnZJ4fjfg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "79xAQpQmKqNnBWtH96Fm+onXsCS7ZTK0CfhCIe3BC56whCMwyh52M/+Qj/xOGhbFnPpjEzJhPnoL1jppniyRAw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte90": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "AMFd0yjzXUV26i0Yzr5MBdolXtz92NZWUvacohGRFFHIHaa8BkKl1c40nw9m33l4cXcwMECsvPkhAcfietYkQg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte90a": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "8rZmZEBu8uDMF1Fq1CITa540cJA8lVj9d9B2D2IgDL6heGkBfQc1nBf2BRcJT81SS9c0wEI1VLIVe3WSPeYG5g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, - "MessagePack": { - "type": "Transitive", - "resolved": "2.2.85", - "contentHash": "3SqAgwNV5LOf+ZapHmjQMUc7WDy/1ur9CfFNjgnfMZKCB5CxkVVbyHa06fObjGTEHZI7mcDathYjkI+ncr92ZQ==", - "dependencies": { - "MessagePack.Annotations": "2.2.85", - "Microsoft.Bcl.AsyncInterfaces": "1.0.0", - "System.Collections.Immutable": "1.5.0", - "System.Memory": "4.5.3", - "System.Reflection.Emit": "4.6.0", - "System.Reflection.Emit.Lightweight": "4.6.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2", - "System.Threading.Tasks.Extensions": "4.5.3" - } - }, - "MessagePack.Annotations": { - "type": "Transitive", - "resolved": "2.2.85", - "contentHash": "YptRsDCQK35K5FhmZ0LojW4t8I6DpetLfK5KG8PVY2f6h7/gdyr8f4++xdSEK/xS6XX7/GPvEpqszKVPksCsiQ==" - }, - "Microsoft.Alm.Authentication": { - "type": "Transitive", - "resolved": "4.0.0.1", - "contentHash": "G+EwnnZeoee3McrWZQ0TOtQShJyCzFwhJdEXIbz5t+G2e67KICipAHfflDSYwokB43YK+y5J7ArnRHDLYtNliA==", - "dependencies": { - "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.8" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "16.5.0", - "contentHash": "K0hfdWy+0p8DJXxzpNc4T5zHm4hf9QONAvyzvw3utKExmxRBShtV/+uHVYTblZWk+rIHNEHeglyXMmqfSshdFA==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.2", - "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "16.6.1", - "contentHash": "nBYXDgAZCfjsOVzlhMB5olGvX4dTDWB/gWaYS/MhgXBcCz8XJuVGqkfK8LmwlBR/eeUPE9Q/NFZNwlJyMZf0vg==" - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.IdentityModel.Clients.ActiveDirectory": { - "type": "Transitive", - "resolved": "3.13.8", - "contentHash": "RAYmpEVVnl8qWdPmm62k9wm6MUB9H4Gk3l7uBADDCCQJDOkpTAINzgWHTBF8hkaTHFLaGIfgOeeLcKT+K1PG7A==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" - }, - "Microsoft.ServiceHub.Analyzers": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "LQsmEP/5i9PvM6O1dx69Yj3C0z/tSWiaLjoX31jQ+ilJZ8x7yqthYOnWaQpeZKxJn+oFxymzGtXgPasnqYM/ww==" - }, - "Microsoft.ServiceHub.Client": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "hYqQlgUhnTq7VHYfIBvuWCwAiTjqhCfEX7d/ISVtEGEv7/N89QAbL+0XCz2NZRN6yMDtVMEoee5Q4k6/uwWlJg==", - "dependencies": { - "Microsoft.ServiceHub.Framework": "3.0.3078", - "Microsoft.ServiceHub.Resources": "3.0.3078", - "Microsoft.VisualStudio.RemoteControl": "16.3.32", - "Microsoft.VisualStudio.Telemetry": "16.3.176", - "StreamJsonRpc": "2.7.70", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.ServiceHub.Framework": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "RMBx+TEE3Fl6CRd1d1ZWKnNPRbPL23NFydDEEjRtZdwTSWe1x0gkUqnGU/ZgtqSsgWUfaQtEPxd8S9qfPGkz0Q==", - "dependencies": { - "Microsoft.ServiceHub.Analyzers": "3.0.3078", - "StreamJsonRpc": "2.7.70", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.ServiceHub.Resources": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "02mGIKyVfnXFEeicpV2RbZapHd6vcefFSSZvjAA+O0kWgB9x2D5Pd3M94Il9LiLgFnw3mmxtf68tbEjOhQ0rWg==" - }, - "Microsoft.VisualStudio.CommandBars": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "+eBuWROC04s6NyVhJW/GJIqy8gOhFaxkWiHtuEf2bVTGkKQ/gx/Rjfegj79H/xEPnBmvbKp8e5+dxJ1WA4r1XA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.ComponentModelHost": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "mIvfzFYYtPbf+aZRLjtOmMfksKPGML2U2z7RqHJ+m0AVTumssJbOpaD5gOgYcUvE6+AjFoUpg+NLVjwxdZnVYQ==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.CoreUtility": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "qk4BeMGWIklu4ia8zf6JPLUGXuJcbjhqfYhIHYbDKETmGxRHoNsvzGBIZT8GkS4VSjcibOFnbCowJfNWy5TfhQ==", - "dependencies": { - "Microsoft.VisualStudio.Threading": "17.0.63", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "YipUF2uyw4MH3WFZf+/4lC9jcHjkL8GgPdwvH800x+kVHAAVIEXJTqPTmsJnBrT/QqXdPHQrM6Srae0VQjG3gA==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "NENWpbG7TvdF1TwesraECA+PgprCLR3Mfts7t8fL5KGc59QiRCeZTpVWBzotvRLMQ0lhA0VYYh1ErDV9D+LM2g==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "Os4TTwz9mrtS5AzTKMyEr2NPIJmmBGlf+Cn4eFPn6t4yMfDYfaFRnOtDgpiF7IyduB+mywO8v+wMLWiQuJth9Q==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.14.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "UwQukQ2/HdIxKqHGDQ8oipxrJ4IWLdQfBBuY8Yl61SAVhcGzDP4Z/XkB7RSIbJOAeFDzu/kuKZ3zXR7jXazAJA==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "P7C071a0dx7TP9NEBmt8pR3BZMOIqQYNWYW1pvxaY6jOb99/q7sm/KgYDKGrK/HEBefbn6g9T6hrVlUeP2GsWQ==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.10.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.16.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "eYE5pOBbWacuf0pUyQHJH9LuP15x67CUy8VJv9AeJM3E91Y95Ff5hkCasBZtf9TJ9uMVnx0+6PdZLkZdYcS4Yg==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.InteropA": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "y1CLio4/zI5PYWt760mJhduq/gpNZNrDwX2c2kBB5p+D8a1ExH4d2tNDTqbQdciOiNR47YenAcSUpyfHv9pNkg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Designer.Interfaces": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "eeNZCUX6RJGnc6YC6Bfs8qbEVt7WxWRPXEbEBjyOqCDU4dJPkR1OLVWMUHkA4XM3sXKImv1LmAwHZk3cWK5iGQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Editor": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "UuIMD8xLAkvwuszD7jlc4ADRckr59eyJ7YJFepTtja2IDMzt5SAGa0kxxjUQ3Zeg0KuhuwcZBwuYmSCgx5YSjA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.GraphModel": "17.0.31723.112", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31723.112", - "Microsoft.VisualStudio.RpcContracts": "17.0.51", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "Microsoft.VisualStudio.Text.UI.Wpf": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.63", - "Microsoft.VisualStudio.Validation": "17.0.28" - } - }, - "Microsoft.VisualStudio.GraphModel": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "53cv+WBrXiX3Bomk2W+gz74tFqFa54OvJ++u4RYAdntvXb/hx+m1EhBgtHKOFu057rVxAq/YNFY7vLCUusQGoQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.ImageCatalog": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "nNfMuMiuC0UX/r5ryjIuqWyj/Wzrz63wuEcX70NtICUNmm/Bc5blXmgH7Et+ArVzZlXi2ExNqbLAi60IpNdaYw==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Imaging": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "NCJX5aUZxcj32Al5E9PI2XONEjR8gQyzjGL2NfNxw8PzDPd6yMmU96y7rnvv0dclPUw0gCcVMtLnNwpCaeXBdg==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Threading": "17.0.64", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "d9I8RzOeF9z1L/QJ3nWOMvJphsxe31cU7WBZW7aM0VTWZUWcYwYQH0CZWFPZsXgFWWRgiK5WL7FiMnYJfeDutw==" - }, - "Microsoft.VisualStudio.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KWBjmU85KpWJiqRgkvuJfQgW8/1RtkFnJ4e4EIcoXYHab/1tinXv219midxHJoc6Sa6E/7Uo3Ku4bWBQMFE9dA==" - }, - "Microsoft.VisualStudio.Language": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "j8hL3Wwst8hwstbBzY24WV0PWYkcWDAC/Y2JHdO9jdlBjvHCHUPN4zNtu6A9oCTNsyLMxzK+pq+X45FFVTeqrA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "Newtonsoft.Json": "13.0.1", - "StreamJsonRpc": "2.8.28", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0", - "System.Private.Uri": "4.3.2" - } - }, - "Microsoft.VisualStudio.Language.Intellisense": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "iPi01Ep2YM4rgL6z26s4eEy8hOYwt5jE1aLebknCnBzUAB3avrSVa1AcOo726SdVN8h/h0lVRE4LMrC4WAQ4rQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "Microsoft.VisualStudio.Language.NavigateTo.Interfaces": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "5/3AstEbNcaZtwwONLcHXc2Nzels7j89jzXsYsxPQCSW9inbDbQoBC6+wtsAwKxOu1BeKpPaGi7bmc7Y+R+vSA==", - "dependencies": { - "Microsoft.VisualStudio.Imaging": "17.0.31723.112", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Logic": "17.0.448", - "Microsoft.VisualStudio.Utilities": "17.0.31723.112" - } - }, - "Microsoft.VisualStudio.Language.StandardClassification": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "n7l7tLV69+FtdH+/SPR8MdVc3UsAvlrgE38mMlXtW3dkxx5BYnSiHl93vZ4omDbX45Rzu9b4DDR1HMzg3GaKzQ==", - "dependencies": { - "Microsoft.VisualStudio.Text.Logic": "17.0.487" - } - }, - "Microsoft.VisualStudio.LanguageServer.Client": { - "type": "Transitive", - "resolved": "17.0.5158", - "contentHash": "cZNQHkvjpCnEnxZrLBflAAXc8CwJYMUgDEcCTgrY86Zpa6tHfaHpMoK3aBXtBlzlJ25vjeqBoilXbbCrPrHnJg==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.448", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31723.112", - "Microsoft.VisualStudio.Utilities": "17.0.31723.112", - "Microsoft.VisualStudio.Validation": "17.0.28", - "StreamJsonRpc": "2.8.28" - } - }, - "Microsoft.VisualStudio.OLE.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "lhZX3Vxm+bRP7/lb8zYG2DlLx3Ea1tnatV+SUYAJ+sL8Qr7v8VBmZurPxGsKYh5Q7ePNjOlkWk2eSaK6TaxUyg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Package.LanguageService.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ZR8l9shqIHXGzfd4Wx9H7Bkgd4ydsMC+Sh8wk05jOV3qS9j9cYTNkRKXOny9rK7lZJIQcH8fPYpIe48kAwZPmg==", - "dependencies": { - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.ProjectAggregator": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "C96eQGjS87DX1yjl/C57YFpWX2hD2Vi0BlLnz44nFhAxCvQ2Fy5FN/E63yNMIdX+iAZkCoG80c1D7C6fBOL0XA==" - }, - "Microsoft.VisualStudio.RemoteControl": { - "type": "Transitive", - "resolved": "16.3.41", - "contentHash": "Q9lz2anDPJxDLznQRaybv21aY3qgQJmGJiUonH8z2D0XAgKMlMelsu9bg9zhnKCxtA/jreRAM3Md2W6thiDOwQ==", - "dependencies": { - "Microsoft.VisualStudio.Utilities.Internal": "16.3.23" - } - }, - "Microsoft.VisualStudio.RpcContracts": { - "type": "Transitive", - "resolved": "17.0.51", - "contentHash": "4cMhOmJJl18BU+LTrcjNTLDqWBuCN5t87fB64n7UNyKLs03o4UVNKEjsUwlo6USbccGfoyba4mWPWIo7vL0qkA==", - "dependencies": { - "Microsoft.ServiceHub.Framework": "3.0.2061", - "StreamJsonRpc": "2.8.28" - } - }, - "Microsoft.VisualStudio.SDK": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "YSOMLjLm0k4Q5JeoPvyRrYaHdUzxkKtkt9Hn/0NjWDdQ7tLrhZjR3SOiaSMYOFYQCQ0G7v2UBIRHfbGAnt7SNg==", - "dependencies": { - "Microsoft.VisualStudio.CommandBars": "17.0.31902.203", - "Microsoft.VisualStudio.ComponentModelHost": "17.0.487", - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Debugger.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.14.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.16.0": "17.0.31902.203", - "Microsoft.VisualStudio.Designer.Interfaces": "17.0.31902.203", - "Microsoft.VisualStudio.Editor": "17.0.487", - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.Language.Intellisense": "17.0.487", - "Microsoft.VisualStudio.Language.NavigateTo.Interfaces": "17.0.487", - "Microsoft.VisualStudio.Language.StandardClassification": "17.0.487", - "Microsoft.VisualStudio.LanguageServer.Client": "17.0.5158", - "Microsoft.VisualStudio.OLE.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Package.LanguageService.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31902.203", - "Microsoft.VisualStudio.Setup.Configuration.Interop": "3.0.4492", - "Microsoft.VisualStudio.Shell.Design": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.11.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.8.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.9.0": "17.0.31902.203", - "Microsoft.VisualStudio.TaskRunnerExplorer.14.0": "14.0.0", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.TextManager.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.11.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.8.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.9.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextTemplating.VSHost": "17.0.31902.203", - "Microsoft.VisualStudio.VCProjectEngine": "17.0.31902.203", - "Microsoft.VisualStudio.VSHelp": "17.0.31902.203", - "Microsoft.VisualStudio.VSHelp80": "17.0.31902.203", - "Microsoft.VisualStudio.Validation": "17.0.28", - "Microsoft.VisualStudio.WCFReference.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Web.BrowserLink.12.0": "12.0.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.ComponentModel.Composition": "4.5.0", - "VSLangProj": "17.0.31902.203", - "VSLangProj100": "17.0.31902.203", - "VSLangProj110": "17.0.31902.203", - "VSLangProj140": "17.0.31902.203", - "VSLangProj150": "17.0.31902.203", - "VSLangProj157": "17.0.31902.203", - "VSLangProj158": "17.0.31902.203", - "VSLangProj165": "17.0.31902.203", - "VSLangProj2": "17.0.31902.203", - "VSLangProj80": "17.0.31902.203", - "VSLangProj90": "17.0.31902.203", - "envdte": "17.0.31902.203", - "envdte100": "17.0.31902.203", - "envdte80": "17.0.31902.203", - "envdte90": "17.0.31902.203", - "envdte90a": "17.0.31902.203", - "stdole": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Setup.Configuration.Interop": { - "type": "Transitive", - "resolved": "3.0.4492", - "contentHash": "BfkqM96P8+N+cz4T+pxKrIKk2ZD1YMxCXH2ivtBDj5tx6Mc2YQLK1+3h+C6Qebper0RBipuHVn51lb9SZH6bKQ==" - }, - "Microsoft.VisualStudio.Shell.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "9bOKENGcO474GIgVoRzmkce6FdW/fiH/W2M0ULyRkFjNh9yEdJheYeyJFBr3Pb0EAwPwu2Q3gayQB7I9oTd/SA==", - "dependencies": { - "Microsoft.VisualStudio.ComponentModelHost": "17.0.487", - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Design": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "gFVdvJ9HOOnq4ntBCcULraptK+V+Mu1GahUXmd0dhsomGzkzgqDqN7aYQ3ys+K45pJhNeB3MJ/NNQUI4woyiBQ==", - "dependencies": { - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Framework": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "L9s0/zSMeNlh1Pyzh9vX7T2+O3vg0JxTgXHyXGP/36DpJh4f+87D4rj+/nkESY4YXIUKPVl7XWXh3NLG0yjj1w==", - "dependencies": { - "Microsoft.Build.Framework": "16.5.0", - "Microsoft.VisualStudio.GraphModel": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Telemetry": "16.3.250", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.64", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203", - "Newtonsoft.Json": "13.0.1", - "System.ComponentModel.Composition": "4.5.0", - "System.Threading.Tasks.Dataflow": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.VisualStudio.Shell.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "GaGSme4K2Wy0b65evinVk7JCl1+Dn7TCxBBwKuzATjWWiY0lvvqLElhXBq+qpC23YN0I5jTBTv0inhZ3G06uMg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "CiHRisP0ts0lbyZ1SR0H7zfiVDTgkQTeORQTDY1xOTy7WL1hIzqmUz/M5UL7d6WcVI4hTAFosJ77NcqYrnTwGg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "doH7HCWzS2bXohKR0Vl1DRjzqV0iw/poZteLS0i4nzPqFs8e2xg+U6a/ysUQ3QHgZ5y7iraefPPmp/VMJfbIeQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "SfZOz3igcZc7mKOBOr2MrPnliJL0HWF0s9UAL1B05Rc8cI/wBNIbyC1PE8Xt44P6c1vjJm6tWscAFSHZdKpGuA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.8.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "rLDc0KVPc1HZEWd5OYyvKGJ2+0eay9gNeh2wMiEJC7UNLHo+JM0wxAYNehze50hWJKlFqefHKJ6Klmp1iX3kpQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.9.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "bz8tlDefGkBIGfF5KSScfs6f7P79IYzX3mqKAlXUshZjYQNd+8hLngls19cCP4oNlOre/DXAhESptNOgVihzzg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TaskRunnerExplorer.14.0": { - "type": "Transitive", - "resolved": "14.0.0", - "contentHash": "iZpAv8bEWjkyxFF1GIcSOfldqP/umopJKnJGKHa0vg8KR7ZY3u3dWtJmwO4w3abIx+176SIkQe78y5A+/Md7FA==" - }, - "Microsoft.VisualStudio.Telemetry": { - "type": "Transitive", - "resolved": "16.3.250", - "contentHash": "Ijo4HUinCwSdgegeXXzfEYmWuVLC1o9CI3FXW2x4CPKoYemlrN6xcGDUy4oKRxyOsFkjAaiRxR/X9TOgy2xVsQ==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Microsoft.VisualStudio.RemoteControl": "16.3.41", - "Microsoft.VisualStudio.Utilities.Internal": "16.3.23", - "Newtonsoft.Json": "9.0.1" - } - }, - "Microsoft.VisualStudio.Text.Data": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "lrqBhf4lI8Xzk0Z5hwcAtqnYTL1OK+iwu6tHJXDR8Vv7hFvG/YwetOQMJPz6UaLd93PEX+jPh0IfdY/CddoVDQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.63" - } - }, - "Microsoft.VisualStudio.Text.Logic": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "ZLMg3fDYijfur0B02/tkB1lmUWQtRfC9xkRr2XlQ/x7MOotTvDQO3HJp9hZyqn9LVBTWbjOnLVcoSmzthsGnnw==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Text.UI": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "f+7L6k+eyJtQzrr//j2ZtYRDnGJeGSluT10do9/3ajgfT3WpRdlq5HEr60imnHbUU5eY2V/bjbKIu8q6Yg2VfA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Text.UI.Wpf": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "Aq+eNxbpBlU8RYsSwh2JM1ZsttVvU7BKgFG6I/356EaY1ZJVz2LhAfjAl6C/A3pmizxgcczQZOIIJsrhcP/OIQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487" - } - }, - "Microsoft.VisualStudio.TextManager.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "w4+TDaisrknpeXkeebsTQsgl+6gbJivFyZp18/7Yqpo/lyG+IttvE9ZnJ5fFofTyrmgSGLi9BLEifT0mtsEaXA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ODXNlirs3ttpe1ooEDsKKw6XbQ06h3kvJua++J0gPqM6vMWS5YfJVH0UxHQ+BDAFZQnUJcbH9b0sVdC9JUKQ6g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "7NvcHy27r3fmlgdxPrZy4LWAm3yFqNJB3vMTMJv0K/olUzIpNRXYJMeQNltb8AJuOrbpb6HV6L2MIyJDehEADg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "2ptvBpyBVEtxwUS+FfFKPjbj+l6u/3IyznQegMmEIoQda6oeg07TFL0VAVmsqg48s4FFtpqRklx8kWhMNZqSXQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.8.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "70mNZyDPIsC+rNov2wHYb6H+V8Woc/FJktj2JsA194eMxk+DPpF0Jx3/sMgkDycV+CjIJog6TguywF4q4milIA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.9.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "s59cD2jJpwEGs6HE0fbp0sE+ZsR4pp8bK91WCelRUVCiMNMtgzb3PVBCjvl082f9UxIsClOBBYDTUjrt/CFqVQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "IYWBsY2RWM8p2blyT0ja94+YRSc0wmL5er4/DfQShc3JkrQpVmmnWwSjRWC5IEp/dSukMW1LEfStqi/Kc6ZTtQ==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "rCQLkYDAKJ4BGot0CL5dkYhwArR3x2COIiTF5z3cMONVVfQqoXJKXaXve/O22DaTE/28Zt5EnaDNzZHIwROpeg==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KsY6a4Nk+wOj/0aGAE21JSglQt6+tlsAyPnVyiqqHn+ir1Wx9o0rFtekc6EfSle8nn5RM7QqLGSUR/Bxy6rQbA==" - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "pDYRlZTH42mS+r+dIjce6KOt/3HSxX1M1tPldT8VySnQ2PsT/6kRK2UVMOPla9hfJ1dWj2wMpYF8UNN2nuzdjA==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces.10.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.VSHost": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "srtrkd9VCmGFRvMNV6STSe0XqM/DSl7xfY+EUIWhPVO8k+dl+DBNPIZzCK43Enfb+i3OrYFY2yqB6baNaelnaA==", - "dependencies": { - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203", - "Microsoft.VisualStudio.TextTemplating": "17.0.31902.203", - "Microsoft.VisualStudio.Validation": "17.0.28", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "Microsoft.VisualStudio.Threading": { - "type": "Transitive", - "resolved": "17.0.64", - "contentHash": "HD/yoC7u1Ignj/EsCST4iFXl8zaE+8r2A+4CUkl6GLTJjdNjfl8iNvhqpyK8+DjCMwhyNRRH0I6S6FA37fz95Q==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.VisualStudio.Threading.Analyzers": "17.0.64", - "Microsoft.VisualStudio.Validation": "16.10.35", - "Microsoft.Win32.Registry": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Transitive", - "resolved": "17.0.64", - "contentHash": "+xz3lAqA3h2/5q6H7Udmz9TsxDQ99O+PjoQ4k4BTO3SfAfyJX7ejh7I1D1N/M/GzGUci9YOUpr6KBO4vXLg+zQ==" - }, - "Microsoft.VisualStudio.Utilities": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "j0c5dq1ZmFFAi4kB4wJ1BOUrb1kJyhFvaqDKymbdJNaib8DdG8PNAdLrW1l0RewsEMDy5Rh2cO7mzrG/FlyE5Q==", - "dependencies": { - "Microsoft.ServiceHub.Client": "3.0.3078", - "Microsoft.VisualStudio.RpcContracts": "17.0.51", - "Microsoft.VisualStudio.Telemetry": "16.3.250", - "System.ComponentModel.Composition": "4.5.0", - "System.Threading.AccessControl": "5.0.0", - "System.Threading.Tasks.Dataflow": "5.0.0" - } - }, - "Microsoft.VisualStudio.Utilities.Internal": { - "type": "Transitive", - "resolved": "16.3.23", - "contentHash": "AxbS8vXJj0IjTv67JbmOqwJERYUDE7BHbXYkXGiyqYblizMjhVdohNIethnJX9lVN2RmotN5GQbwLWDoMKatvw==" - }, - "Microsoft.VisualStudio.Validation": { - "type": "Transitive", - "resolved": "17.0.28", - "contentHash": "qT+0Qv7lxLt7NKQjkroi34s8cDXVPWA3vDkvoFZwM9PRmZ28aKrMLaQRnkT7rgBYLf+mNtr2najktKUzkAtP6Q==" - }, - "Microsoft.VisualStudio.VCProjectEngine": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "QjjZguY7FypxVasdPihUbVSP5PjwBPiitCgnC/ZPePFO5KYzJ99VxdlASDDxNGVq5+q/+3YlBD94PXComDRu2w==" - }, - "Microsoft.VisualStudio.VSHelp": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "PT4aoOrZfPHIpTq3lwamypIrx0hR7d5iLlHewwhjUNezQ8ElN/jbWjvx74r/uf2g6dVPiQcS4HI0RGgAb4MQ2A==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.VSHelp80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "B/G69KkAcMIwXgEvGAMpGSq1PvMx9JKAVzQdKiX4gm2DMFEsT2Id89TxS0oPg2X7QC3JBCk+/+BDUjtWPkjqMA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.WCFReference.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "45HixSYn5IBSgc6MmVHhivWCiRgdZ8BruXOC9fUrQIRrYHu8DR9aw8imnFImmJY3Mii92VHV4NKPYOgUkXNd/Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Web.BrowserLink.12.0": { - "type": "Transitive", - "resolved": "12.0.0", - "contentHash": "HeuaZh8+wNVdwx7VF8guFGH2Z2zH+FYxWBsRNp+FjjlmrhCfM7GUQV5azaTv/bN5TPaK8ALJoP9UX5o1FB5k1A==" - }, - "Microsoft.Web.Xdt": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "/ieJ02r4MEJM21Eyl+c5kwoJWPhy+qEEcN68JaqDoamabgxJI1jGi/kNLuKvHUCl6tU7E0rMvaR6FmEnDWtS4A==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==" - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Nerdbank.Streams": { - "type": "Transitive", - "resolved": "2.6.81", - "contentHash": "htBHFE359qyyFwrvAGvFxrbBAoldZdl0XjtQdDWTJ8t5sWWs7QVXID5y1ZGJE61UgpV5CqWsj/NT0LOAn5GdZw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.VisualStudio.Threading": "16.7.56", - "Microsoft.VisualStudio.Validation": "15.5.31", - "System.IO.Pipelines": "4.7.2", - "System.Net.WebSockets": "4.3.0", - "System.Runtime.CompilerServices.Unsafe": "4.7.1" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "NuGet.Core": { - "type": "Transitive", - "resolved": "2.12.0", - "contentHash": "kSDD1VFIq7BBrimjMRk9HeeQlZteyknbHgz3AT4AUVUCZ8BZaCSzO1A5UTVPLLbSHryjozPcEmNYSds/IDAIjw==", - "dependencies": { - "Microsoft.Web.Xdt": "2.1.0" - } - }, - "NuGet.VisualStudio": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "ZVBJ43/IxaAl5iX+OuuDLQ9tSNJaRlP85SUMAdiGoqEbyK/ZPfQmPfqSMZMFpucRlf8HgsEK/hR73DwpCmqcLA==" - }, - "stdole": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "c1WK5n8poXt1fafyuS+ntL5w0tc3P/r4F7+aeUaLYTp2/YoQB8XUpQFWqytZdtu7EAuMXoZ14T7jh7Vl+M4ZfQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "StreamJsonRpc": { - "type": "Transitive", - "resolved": "2.8.28", - "contentHash": "i2hKUXJSLEoWpPqQNyISqLDqmFHMiyasjTC/PrrHNWhQyauFeVoebSct3E4OTUzRC1DYjVJ9AMiVbp/uVYLnjQ==", - "dependencies": { - "MessagePack": "2.2.85", - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.VisualStudio.Threading": "16.9.60", - "Nerdbank.Streams": "2.6.81", - "Newtonsoft.Json": "12.0.2", - "System.Collections.Immutable": "5.0.0", - "System.Diagnostics.DiagnosticSource": "5.0.1", - "System.IO.Pipelines": "5.0.1", - "System.Memory": "4.5.4", - "System.Net.Http": "4.3.4", - "System.Net.WebSockets": "4.3.0", - "System.Reflection.Emit": "4.7.0", - "System.Threading.Tasks.Dataflow": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "StrongNamer": { - "type": "Transitive", - "resolved": "0.0.8", - "contentHash": "7GAMIhQSKabPrXVtEpDQoLRWVZUn1IGzeHvfDSvs5lPhU4KPFMQVpI/lDFikhMXr8FmdFBOBGaHEhQ9azMK3TA==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", - "dependencies": { - "System.Memory": "4.5.4" - } - }, - "System.ComponentModel.Composition": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "uXQEYqav2V3zP6OwkOKtLv+qIi6z3m1hsGyKwXX7ZA7htT4shoVccGxnJ9kVRFPNAsi1ArZTq2oh7WOto6GbkQ==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==" - }, - "System.IO.Abstractions": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" - }, - "System.IO.Abstractions.TestingHelpers": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "jygAL7t8okBFhnxzAP7gxDHKCrT2UZWcJCvXfFSf2M9QfOoryfvMyvRYIdu7B/+wYPrCbhSOJDQy5pwdy0bhQg==", - "dependencies": { - "System.IO.Abstractions": "9.0.4" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.4", - "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", - "dependencies": { - "System.Security.Cryptography.X509Certificates": "4.3.0" - } - }, - "System.Net.WebSockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "u6fFNY5q4T8KerUAVbya7bR6b7muBuSTAersyrihkcmE5QhEOiH3t5rh4il15SexbVlpXFHGuMwr/m8fDrnkQg==" - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Private.Uri": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "o1+7RJnu3Ik3PazR7Z7tJhjPdE000Eq2KGLLWhqJJKXj04wrS8lwb1OFtDF9jzXXADhUuZNJZlPc98uwwqmpFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", - "dependencies": { - "System.Collections.Immutable": "5.0.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==" - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.2" - } - }, - "System.Threading.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "WJ9w9m4iHJVq0VoH7hZvYAccbRq95itYRhAAXd6M4kVCzLmT6NqTwmSXKwp3oQilWHhYTXgqaIXxBfg8YaqtmA==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NBp0zSAMZp4muDje6XmbDfmkqw9+qsDCHp+YMEtnVgHEjQZ3Q7MzFTTp3eHqpExn4BwMrS7JkUVOTcVchig4Sw==" - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "VSLangProj": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "XjyhxIyXjPitKrxeWkF1yaiqT02Klct5yZHz93x9O4nLTEnbmAHb+HRNiIIgNMXvG2sboNIvTt1MpuR1AeJktw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj100": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "8pwHJP2XefsWxhL3dQ11MBD9WwxP3SjAO41t6ZcFSfTHtSQFsp7erdtBrKzuFXpnIp3WoVc26f7jWrt4rr66+g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj110": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "UToccqkFdocw9LZx7Rn62/puqxPjF7MQWIk2yMGqIdHLcaeRmR6gI1/3Ea+57ZcVehhuEvtxH8jXV2t4HwwU8g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj140": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ssRCHLYW5nSJJBy3rad5JId8SjcXHOsOzVW4ohZgtnHBtf73ehIJLtr+oRc7JK3NasmWBuB9DJqwwJBLj4wn3Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj150": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "sxFOk5x5bDqyxUgfi5zF58E8BnfRQ17Ft0IbxJDS0Bvf+6CLYjWQzKXqxCczYHs2aD7upUpN1hrf93bHlqahAQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj157": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "jYp9X00aaJKttYL1IuME8mpvd349H2zDbaFIBRf6LnDUPHd7i7MsnaREpID1Dr+H37P16pzbv48taLKUnAoBsw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj158": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "bwHHmgclOjBNI/CpUqgPH274D7UKDrgzFAEtFULixlOhA6E4NfYOgdUdemiW5bg58i4ao60T9tz3ac6PQfHeAw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj165": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "Brh37fZOyPL0zC9XVtOzWRl22GZZnN6/TVEvg6PDhWhjgnkdHv8hz0GZCIzilbyl9ilOJnQCx8oVQ+KMfcvXFQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj2": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "hqP1IODtJneTrUwwXeqD0YHmqyde3/KJaJRJehsn07TQxYxzyigrpNsh8GEoExTSis0SoFt97tXTztbXiBvSvg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "qm0T7dPige52L6mnsk+3Q8QV8R/Rgdkdpb3FaUMolP4JWU4s2OfrUzFajFBf8auf/NBQgUcBvE1Kr5yRpYmL2Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj90": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KoyI+jvJ9fHwR2uGGlUxOqAmTgDO1naxwXbt504y/Jx1h/BXnhCWipGdyM/lS83GUGsmx8RhZbiiMlZw7LmYhw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "SonarLint.VisualStudio.ConnectedMode": { - "type": "Project", - "dependencies": { - "Microsoft.Alm.Authentication": "[4.0.0.1, )", - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", - "SonarLint.VisualStudio.SLCore": "[1.0.0, )", - "StrongNamer": "[0.0.8, )" - } - }, - "SonarLint.VisualStudio.Core": { - "type": "Project", - "dependencies": { - "BouncyCastle.Cryptography": "[2.4.0, )", - "Newtonsoft.Json": "[13.0.3, )", - "System.Collections.Immutable": "[5.0.0, )", - "System.IO.Abstractions": "[9.0.4, )", - "System.Threading.Channels": "[7.0.0, )" - } - }, - "SonarLint.VisualStudio.Infrastructure.VS": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )" - } - }, - "SonarLint.VisualStudio.Integration": { - "type": "Project", - "dependencies": { - "Microsoft.Alm.Authentication": "[4.0.0.1, )", - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "NuGet.Core": "[2.12.0, )", - "NuGet.VisualStudio": "[3.3.0, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "StrongNamer": "[0.0.8, )", - "System.IO.Abstractions": "[9.0.4, )" - } - }, - "SonarLint.VisualStudio.Integration.TestInfrastructure": { - "type": "Project", - "dependencies": { - "FluentAssertions": "[5.9.0, )", - "FluentAssertions.Analyzers": "[0.11.4, )", - "MSTest.TestAdapter": "[1.4.0, )", - "MSTest.TestFramework": "[1.4.0, )", - "Microsoft.Alm.Authentication": "[4.0.0.1, )", - "Microsoft.NET.Test.Sdk": "[16.6.1, )", - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Moq": "[4.18.2, )", - "NSubstitute": "[5.1.0, )", - "NSubstitute.Analyzers.CSharp": "[1.0.17, )", - "NuGet.Core": "[2.12.0, )", - "NuGet.VisualStudio": "[3.3.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarLint.VisualStudio.Integration": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "StrongNamer": "[0.0.8, )", - "System.IO.Abstractions.TestingHelpers": "[9.0.4, )" - } - }, - "SonarLint.VisualStudio.IssueVisualization": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarLint.VisualStudio.SLCore": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )" - } - }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" - } - }, - "SonarLint.VisualStudio.SLCore": { - "type": "Project", - "dependencies": { - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "StreamJsonRpc": "[2.6.121, )" - } - }, - "sonarqube.client": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "System.Net.Http": "[4.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs deleted file mode 100644 index d840de4eb6..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Logging; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - internal interface IContainer : IDisposable - { - ILogger Logger { get; } - - ISuppressionChecker SuppressionChecker { get; } - } - - internal sealed class Container : IContainer - { - /// - /// We do not provide ValueFactory in the initialization so that exceptions would not be cached. - /// https://docs.microsoft.com/en-us/dotnet/api/system.threading.lazythreadsafetymode?view=net-6.0 - /// - private static readonly Lazy _instance = new Lazy(LazyThreadSafetyMode.ExecutionAndPublication); - - public static IContainer Instance - { - get - { - try - { - return _instance.Value; - } - catch - { - return null; - } - } - } - - private readonly ISuppressedIssuesFileWatcher fileWatcher; - - public ILogger Logger { get; } - - public ISuppressionChecker SuppressionChecker { get; } - - public Container() - { - Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); - Logger = LoggerFactory.Default.Create(new SystemDebugLoggerWriter(), new EnableAllLoggerSettingsProvider()); - - var settingsCache = new SettingsCache(Logger); - fileWatcher = new SuppressedIssuesFileWatcher(settingsCache, Logger); - - SuppressionChecker = new SuppressionChecker(settingsCache); - } - - public void Dispose() - { - fileWatcher?.Dispose(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs deleted file mode 100644 index aa79440005..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/ISuppressedIssuesCalculator.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess -{ - internal interface ISuppressedIssuesCalculator - { - /// - /// Returns the suppressed issues that should be added to the settings file, or null if no changes are needed. - /// - IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey); - } - - internal interface ISuppressedIssuesCalculatorFactory - { - ISuppressedIssuesCalculator CreateAllSuppressedIssuesCalculator(IEnumerable sonarQubeIssues); - - ISuppressedIssuesCalculator CreateNewSuppressedIssuesCalculator(IEnumerable sonarQubeIssues); - - ISuppressedIssuesCalculator CreateSuppressedIssuesRemovedCalculator(IEnumerable issueServerKeys); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs deleted file mode 100644 index a46e02d968..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/IssueConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -// Converts SonarQube issues to SuppressedIssues that can be compared more easily with Roslyn issues -internal static class IssueConverter -{ - public static SuppressedIssue Convert(SonarQubeIssue issue) - { - var (repoKey, ruleKey) = GetRepoAndRuleKey(issue.RuleId); - var language = GetRoslynLanguage(repoKey); - - var line = issue.TextRange == null ? (int?)null : issue.TextRange.StartLine - 1; - return new SuppressedIssue - { - RoslynRuleId = ruleKey, - FilePath = issue.FilePath, - Hash = issue.Hash, - RoslynLanguage = language, - RoslynIssueLine = line, - IssueServerKey = issue.IssueKey - }; - } - - private static (string repoKey, string ruleKey) GetRepoAndRuleKey(string sonarRuleId) - { - // Sonar rule ids are in the form "[repo key]:[rule key]" - var separatorPos = sonarRuleId.IndexOf(":", StringComparison.OrdinalIgnoreCase); - if (separatorPos > -1) - { - var repoKey = sonarRuleId.Substring(0, separatorPos); - var ruleKey = sonarRuleId.Substring(separatorPos + 1); - - return (repoKey, ruleKey); - } - - return (null, null); // invalid rule key -> ignore - } - - private static RoslynLanguage GetRoslynLanguage(string repoKey) - { - // Currently the only Sonar repos which contain Roslyn analysis rules are - // csharpsquid and vbnet. These include "normal" and "hotspot" rules. - // The taint rules are in a different repo, and the part that is implemented - // as a Roslyn analyzer won't raise issues anyway. - switch (repoKey) - { - case "csharpsquid": // i.e. the rules in SonarAnalyzer.CSharp - return RoslynLanguage.CSharp; - case "vbnet": // i.e. SonarAnalyzer.VisualBasic - return RoslynLanguage.VB; - default: - return RoslynLanguage.Unknown; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs deleted file mode 100644 index deeffac062..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculator.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -internal abstract class SuppressedIssuesCalculatorBase : ISuppressedIssuesCalculator -{ - public abstract IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey); - - protected static SuppressedIssue[] GetRoslynSuppressedIssues(IEnumerable sonarQubeIssues) - { - var suppressionsToAdd = sonarQubeIssues - .Where(x => x.IsResolved) - .Select(IssueConverter.Convert) - .Where(x => x.RoslynLanguage != RoslynLanguage.Unknown && !string.IsNullOrEmpty(x.RoslynRuleId)) - .ToArray(); - return suppressionsToAdd; - } -} - -internal class AllSuppressedIssuesCalculator(ILogger logger, IEnumerable sonarQubeIssues) : SuppressedIssuesCalculatorBase -{ - public override IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey) - { - logger.LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerReloadSuppressions); - return GetRoslynSuppressedIssues(sonarQubeIssues); - } -} - -internal class NewSuppressedIssuesCalculator(ILogger logger, IRoslynSettingsFileStorage roslynSettingsFileStorage, IEnumerable newSonarQubeIssues) : SuppressedIssuesCalculatorBase -{ - public override IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey) - { - logger.LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerAddNewSuppressions); - - var suppressedIssuesInFile = roslynSettingsFileStorage.Get(roslynSettingsKey)?.Suppressions; - var suppressedIssuesToAdd = GetRoslynSuppressedIssues(newSonarQubeIssues); - if (suppressedIssuesInFile is null) - { - // if the settings do not exist on disk, add all the new suppressed issues - return suppressedIssuesToAdd; - } - - var suppressedIssuesToAddNotExistingInFile = suppressedIssuesToAdd.Where(newIssue => suppressedIssuesInFile.All(existing => !newIssue.AreSame(existing))); - return suppressedIssuesInFile.Concat(suppressedIssuesToAddNotExistingInFile); - } -} - -internal class SuppressedIssuesRemovedCalculator(ILogger logger, IRoslynSettingsFileStorage roslynSettingsFileStorage, IEnumerable resolvedIssueServerKeys) : SuppressedIssuesCalculatorBase -{ - public override IEnumerable GetSuppressedIssuesOrNull(string roslynSettingsKey) - { - var suppressedIssuesInFile = roslynSettingsFileStorage.Get(roslynSettingsKey)?.Suppressions?.ToList(); - var resolvedIssues = suppressedIssuesInFile?.Where(existingIssue => resolvedIssueServerKeys.Any(x => existingIssue.IssueServerKey == x)).ToList(); - if (resolvedIssues == null || !resolvedIssues.Any()) - { - // nothing to be done if no issue from file was resolved - return null; - } - - logger.LogVerbose(Resources.Strings.RoslynSettingsFileSynchronizerRemoveSuppressions); - return suppressedIssuesInFile.Except(resolvedIssues); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs deleted file mode 100644 index 4176bed23f..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InProcess/SuppressedIssuesCalculatorFactory.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.InProcess; - -[Export(typeof(ISuppressedIssuesCalculatorFactory))] -[PartCreationPolicy(CreationPolicy.NonShared)] -[method: ImportingConstructor] -internal class SuppressedIssuesCalculatorFactory(ILogger logger, IRoslynSettingsFileStorage roslynSettingsFileStorage) : ISuppressedIssuesCalculatorFactory -{ - internal const string SuppressedIssuesCalculatorLogContext = "Suppressed Issues Calculator"; - private readonly ILogger logger = logger.ForContext(SuppressedIssuesCalculatorLogContext); - - public ISuppressedIssuesCalculator CreateAllSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => new AllSuppressedIssuesCalculator(logger, sonarQubeIssues); - - public ISuppressedIssuesCalculator CreateNewSuppressedIssuesCalculator(IEnumerable sonarQubeIssues) => - new NewSuppressedIssuesCalculator(logger, roslynSettingsFileStorage, sonarQubeIssues); - - public ISuppressedIssuesCalculator CreateSuppressedIssuesRemovedCalculator(IEnumerable issueServerKeys) => - new SuppressedIssuesRemovedCalculator(logger, roslynSettingsFileStorage, issueServerKeys); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs deleted file mode 100644 index 270d3beb93..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/InternalsVisibleTo.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Runtime.CompilerServices; - -#if SignAssembly -[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010081b4345a022cc0f4b42bdc795a5a7a1623c1e58dc2246645d751ad41ba98f2749dc5c4e0da3a9e09febcb2cd5b088a0f041f8ac24b20e736d8ae523061733782f9c4cd75b44f17a63714aced0b29a59cd1ce58d8e10ccdb6012c7098c39871043b7241ac4ab9f6b34f183db716082cd57c1ff648135bece256357ba735e67dc6")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] - -#else - -[assembly: InternalsVisibleTo("SonarLint.VisualStudio.Roslyn.Suppressions.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] - -#endif diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs deleted file mode 100644 index a58f8a47c2..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Diagnostics.CodeAnalysis; -using SonarLint.VisualStudio.Core.Logging; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions; - -[ExcludeFromCodeCoverage] -internal class EnableAllLoggerSettingsProvider : ILoggerSettingsProvider -{ - public bool IsVerboseEnabled => true; - public bool IsThreadIdEnabled => true; -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs deleted file mode 100644 index 007098e710..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Diagnostics.CodeAnalysis; -using SonarLint.VisualStudio.Core.Logging; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions; - -[ExcludeFromCodeCoverage] -internal class SystemDebugLoggerWriter : ILoggerWriter -{ - public void WriteLine(string message) => Debug.WriteLine(message); -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs deleted file mode 100644 index 700274fa83..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.Designer.cs +++ /dev/null @@ -1,135 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonarLint.VisualStudio.Roslyn.Suppressions.Resources.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error handling SonarQube for Visual Studio suppressions change. Issues suppressed on the server may not be suppressed in the IDE. Error: {0}. - /// - internal static string FileWatcherException { - get { - return ResourceManager.GetString("FileWatcherException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error deleting the settings file for solution {0}. Error: {1}. - /// - internal static string RoslynSettingsFileStorageDeleteError { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageDeleteError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings File was not found. - /// - internal static string RoslynSettingsFileStorageFileNotFound { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageFileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error loading settings for project {0}. Issues suppressed on the server will not be suppressed in the IDE. Error: {1}. - /// - internal static string RoslynSettingsFileStorageGetError { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageGetError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Roslyn Suppressions] Error writing settings for project {0}. Issues suppressed on the server may not be suppressed in the IDE. Error: {1}. - /// - internal static string RoslynSettingsFileStorageUpdateError { - get { - return ResourceManager.GetString("RoslynSettingsFileStorageUpdateError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Adding new suppressions.... - /// - internal static string RoslynSettingsFileSynchronizerAddNewSuppressions { - get { - return ResourceManager.GetString("RoslynSettingsFileSynchronizerAddNewSuppressions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reloading issue suppressions.... - /// - internal static string RoslynSettingsFileSynchronizerReloadSuppressions { - get { - return ResourceManager.GetString("RoslynSettingsFileSynchronizerReloadSuppressions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Removing suppressions.... - /// - internal static string RoslynSettingsFileSynchronizerRemoveSuppressions { - get { - return ResourceManager.GetString("RoslynSettingsFileSynchronizerRemoveSuppressions", resourceCulture); - } - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx b/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx deleted file mode 100644 index b8d2e133a8..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Resources/Strings.resx +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - [Roslyn Suppressions] Error handling SonarQube for Visual Studio suppressions change. Issues suppressed on the server may not be suppressed in the IDE. Error: {0} - - - [Roslyn Suppressions] Error deleting the settings file for solution {0}. Error: {1} - - - Settings File was not found - - - [Roslyn Suppressions] Error loading settings for project {0}. Issues suppressed on the server will not be suppressed in the IDE. Error: {1} - - - [Roslyn Suppressions] Error writing settings for project {0}. Issues suppressed on the server may not be suppressed in the IDE. Error: {1} - - - Adding new suppressions... - - - Reloading issue suppressions... - - - Removing suppressions... - - \ No newline at end of file diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj b/src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj deleted file mode 100644 index 5261268bf8..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Roslyn.Suppressions.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - {082D5D8E-F914-4139-9AE3-3F48B679E3DA} - SonarLint.VisualStudio.Roslyn.Suppressions - SonarLint.VisualStudio.Roslyn.Suppressions - true - true - - - - - - - - - - - - - - True - SupportedSuppressionBuilder.tt - - - - - - True - True - Strings.resx - - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - - - - - - - - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs deleted file mode 100644 index eed0b94c92..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/RoslynSettings.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Data class - contains all information that needs to be passed between the main VS - /// process and the external Roslyn analysis process - /// - internal class RoslynSettings - { - public static readonly RoslynSettings Empty = new RoslynSettings { Suppressions = Enumerable.Empty() }; - - /// - /// The actual Sonar project key to which the settings relate - /// - /// Note: this is not necessarily the same as the "settings key" - the settings key is a modified version of the - /// project key without any invalid file characters. - [JsonProperty("sonarProjectKey")] - public string SonarProjectKey { get; set; } - - [JsonProperty("suppressions")] - public IEnumerable Suppressions { get; set; } - } - - // Used as a parameter in data-driven tests, so needs to be public - public enum RoslynLanguage - { - Unknown = 0, - CSharp = 1, - VB = 2 - } - - /// - /// Describes a single C#/VB.NET issue that has been suppressed on the server - /// - /// This class contains the subset of fields from - /// needed to match suppressed issues against "live" Roslyn issues - internal class SuppressedIssue - { - /// - /// Relative file path - /// - /// - /// The path is relative to the Sonar project root. - /// The path is in Windows format i.e. the directory separators are backslashes - /// - [JsonProperty("file")] - public string FilePath { get; set; } - - [JsonProperty("hash")] - public string Hash { get; set; } - - [JsonProperty("lang")] - [JsonConverter(typeof(StringEnumConverter))] - public RoslynLanguage RoslynLanguage { get; set; } - - /// - /// The rule ID reported by the Roslyn analyzer e.g. S123 - /// - /// This Sonar rule key without repository key. - [JsonProperty("rule")] - public string RoslynRuleId { get; set; } - - /// - /// The 0-based line for the issue. Will be null for file-level issues - /// - /// Roslyn issues are 0-based - Sonar issues are 1-based. - [JsonProperty("line")] - public int? RoslynIssueLine { get; set; } - - /// - /// The key of the issue from the server - /// - [JsonProperty("issueServerKey")] - public string IssueServerKey { get; set; } - - public bool AreSame(SuppressedIssue other) - { - if (other == null) - { - return false; - } - return FilePath == other.FilePath && - Hash == other.Hash && - RoslynLanguage == other.RoslynLanguage && - RoslynRuleId == other.RoslynRuleId && - RoslynIssueLine == other.RoslynIssueLine && - IssueServerKey == other.IssueServerKey; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs deleted file mode 100644 index 38edfd8313..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/ISettingsCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache -{ - internal interface ISettingsCache - { - /// - /// Returns the settings for the specified key, or if - /// there are no settings for that key - /// - RoslynSettings GetSettings(string settingsKey); - - void Invalidate(string settingsKey); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs deleted file mode 100644 index ec6cf4b161..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Settings.Cache/SettingsCache.cs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Concurrent; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; -using static SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile.RoslynSettingsFileInfo; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache -{ - internal class SettingsCache : ISettingsCache - { - private readonly IRoslynSettingsFileStorage fileStorage; - private readonly ConcurrentDictionary settingsCollection; - - - public SettingsCache(ILogger logger) : this(new RoslynSettingsFileStorage(logger), new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)) - { - } - - internal SettingsCache(IRoslynSettingsFileStorage fileStorage, ConcurrentDictionary settingsCollection) - { - this.fileStorage = fileStorage; - this.settingsCollection = settingsCollection; - } - - public RoslynSettings GetSettings(string settingsKey) - { - if (!settingsCollection.ContainsKey(settingsKey)) - { - var settings = fileStorage.Get(settingsKey) ?? RoslynSettings.Empty; - settingsCollection.AddOrUpdate(settingsKey, settings, (x,y) => settings); - } - return settingsCollection[settingsKey]; - } - - public void Invalidate(string settingsKey) - { - settingsCollection.TryRemove(settingsKey, out _); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs deleted file mode 100644 index 3a2c63b52a..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using SonarLint.VisualStudio.Core.Helpers; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile -{ - internal static class RoslynSettingsFileInfo - { - public static readonly string Directory = PathHelper.GetTempDirForTask(false, "Roslyn"); - - /// - /// Returns the full file path for the given solution name - /// - /// The solution name without the file extension e.g. "MySolution" - /// File will be in a shared location and would be able to be accessed by multiple instances of VS simulateneously - public static string GetSettingsFilePath(string solutionName) - { - var escapedName = PathHelper.EscapeFileName(NormalizeKey(solutionName)); - - var fileName = escapedName + ".json"; - - return Path.Combine(Directory, fileName); - } - - /// - /// Returns an identifier for the project settings to which the settings file relates. - /// - /// The identifier is *not* the actual project key since we can't recover it accurately from the file name - public static string GetSettingsKey(string filePath) - { - return Path.GetFileNameWithoutExtension(filePath); - } - - private static string NormalizeKey(string key) => key.ToLowerInvariant(); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs deleted file mode 100644 index 63e457b5be..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileStorage.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using System.IO.Abstractions; -using Newtonsoft.Json; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ETW; -using SonarLint.VisualStudio.Roslyn.Suppressions.Resources; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; - -internal interface IRoslynSettingsFileStorage -{ - /// - /// Updates the Roslyn settings file on disc for the specified solution - /// - void Update(RoslynSettings settings, string solutionNameWithoutExtension); - - /// - /// Return the settings for the specific settings key, or null - /// if there are no settings for that key - /// - RoslynSettings Get(string settingsKey); - - /// - /// Deletes the Roslyn settings file on disc for the specified solution - /// - void Delete(string solutionNameWithoutExtension); -} - -[Export(typeof(IRoslynSettingsFileStorage))] -internal class RoslynSettingsFileStorage : IRoslynSettingsFileStorage -{ - private readonly IFileSystem fileSystem; - private readonly ILogger logger; - - [ImportingConstructor] - public RoslynSettingsFileStorage(ILogger logger) : this(logger, new FileSystem()) - { - } - - internal RoslynSettingsFileStorage(ILogger logger, IFileSystem fileSystem) - { - this.fileSystem = fileSystem; - this.logger = logger; - fileSystem.Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); - } - - public RoslynSettings Get(string settingsKey) - { - Debug.Assert(settingsKey != null, "Not expecting settings to be null"); - - try - { - CodeMarkers.Instance.FileStorageGetStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(settingsKey); - - if (!fileSystem.File.Exists(filePath)) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageGetError, settingsKey, Strings.RoslynSettingsFileStorageFileNotFound)); - return null; - } - - var fileContent = fileSystem.File.ReadAllText(filePath); - return JsonConvert.DeserializeObject(fileContent); - } - catch (Exception ex) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageGetError, settingsKey, ex.Message)); - } - finally - { - CodeMarkers.Instance.FileStorageGetStop(); - } - return null; - } - - public void Delete(string solutionNameWithoutExtension) - { - try - { - CodeMarkers.Instance.FileStorageUpdateStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(solutionNameWithoutExtension); - fileSystem.File.Delete(filePath); - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - logger.LogVerbose(Strings.RoslynSettingsFileStorageDeleteError, solutionNameWithoutExtension, ex.Message); - } - finally - { - CodeMarkers.Instance.FileStorageUpdateStop(); - } - } - - public void Update(RoslynSettings settings, string solutionNameWithoutExtension) - { - Debug.Assert(settings != null, "Not expecting settings to be null"); - Debug.Assert(!string.IsNullOrWhiteSpace(settings.SonarProjectKey), "Not expecting settings.SonarProjectKey to be null"); - Debug.Assert(solutionNameWithoutExtension != null, "Not expecting solutionNameWithoutExtension to be null"); - - try - { - CodeMarkers.Instance.FileStorageUpdateStart(); - var filePath = RoslynSettingsFileInfo.GetSettingsFilePath(solutionNameWithoutExtension); - var fileContent = JsonConvert.SerializeObject(settings, Formatting.Indented); - fileSystem.File.WriteAllText(filePath, fileContent); - } - catch (Exception ex) - { - logger.WriteLine(string.Format(Strings.RoslynSettingsFileStorageUpdateError, settings.SonarProjectKey, ex.Message)); - } - finally - { - CodeMarkers.Instance.FileStorageUpdateStop(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs deleted file mode 100644 index 66f3abe902..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SettingsFile/RoslynSettingsFileWatcher.cs +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.IO.Abstractions; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.ETW; -using SonarLint.VisualStudio.Roslyn.Suppressions.Resources; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile -{ - /// - /// Monitors files created under directory and - /// calls when a sonarProject's settings file is changed. - /// - internal interface ISuppressedIssuesFileWatcher : IDisposable - { - } - - internal sealed class SuppressedIssuesFileWatcher : ISuppressedIssuesFileWatcher - { - private IFileSystemWatcher fileSystemWatcher; - private readonly ISettingsCache settingsCache; - private readonly ILogger logger; - - public SuppressedIssuesFileWatcher(ISettingsCache settingsCache, ILogger logger) - : this(settingsCache, logger, new FileSystem()) - { - } - - internal SuppressedIssuesFileWatcher(ISettingsCache settingsCache, ILogger logger, IFileSystem fileSystem) - { - this.settingsCache = settingsCache; - this.logger = logger; - - WatchSuppressionsDirectory(fileSystem); - } - - /// - /// Monitors files created/edited/deleted in folder. - /// - /// - /// This method can throw (for example if the directory does not exist yet). - /// It's a deliberate design choice to not catch exceptions and let the caller handle it. - /// This is because parts of the system need to be singletons and the caller should be the one responsible for handling that. - /// - private void WatchSuppressionsDirectory(IFileSystem fileSystem) - { - fileSystemWatcher = fileSystem.FileSystemWatcher.FromPath(RoslynSettingsFileInfo.Directory); - - fileSystemWatcher.Filter = "*.json"; - fileSystemWatcher.Created += InvalidateCache; - fileSystemWatcher.Changed += InvalidateCache; - fileSystemWatcher.Deleted += InvalidateCache; - - fileSystemWatcher.EnableRaisingEvents = true; - } - - private void InvalidateCache(object sender, FileSystemEventArgs e) - { - try - { - CodeMarkers.Instance.FileWatcherInvalidateStart(e.ChangeType.ToString()); - var fileName = e.Name; - var settingsKey = RoslynSettingsFileInfo.GetSettingsKey(fileName); - - if (!string.IsNullOrEmpty(fileName)) - { - settingsCache.Invalidate(settingsKey); - } - } - catch (Exception ex) - { - logger.WriteLine(Strings.FileWatcherException, ex); - } - finally - { - CodeMarkers.Instance.FileWatcherInvalidateStop(e.ChangeType.ToString()); - } - } - - public void Dispose() - { - fileSystemWatcher.Created -= InvalidateCache; - fileSystemWatcher.Changed -= InvalidateCache; - fileSystemWatcher.Deleted -= InvalidateCache; - fileSystemWatcher.Dispose(); - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs deleted file mode 100644 index 0e544de763..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SonarDiagnosticSuppressor.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core.ETW; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Diagnostic suppressor that can suppress all Sonar C# and VB.NET diagnostics - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - internal class SonarDiagnosticSuppressor : DiagnosticSuppressor - { - /// - /// Func is used so that we could initialize the container lazily only when we are in connected mode. - /// - private readonly Func getContainer; - - public SonarDiagnosticSuppressor () : this(() => Container.Instance) - { - - } - - internal SonarDiagnosticSuppressor(Func getContainer) - { - this.getContainer = getContainer; - } - - public override ImmutableArray SupportedSuppressions => SupportedSuppressionsBuilder.Instance.Descriptors; - - public override void ReportSuppressions(SuppressionAnalysisContext context) - { - CodeMarkers.Instance.ReportSuppressionsStart(); - var executionContext = new SuppressionExecutionContext(context.Options); - - var suppressions = GetSuppressions(context.ReportedDiagnostics, executionContext); - - //SuppressionAnalysisContext is a public struct with an internal constructor and because of that we can't mock or create it - //To be able the test we had to seperate parts of code that do not use SuppressionAnalysisContext directly and had to loop twice - foreach (var suppression in suppressions) - { - context.ReportSuppression(suppression); - } - CodeMarkers.Instance.ReportSuppressionsStop(executionContext.Mode, suppressions.Count()); - } - - internal /*For Testing*/ IEnumerable GetSuppressions(ImmutableArray ReportedDiagnostics, ISuppressionExecutionContext executionContext) - { - if (!executionContext.IsInConnectedMode) - { - return Enumerable.Empty(); - } - var result = new List(); - - var container = getContainer(); - - foreach (var diag in ReportedDiagnostics.Where(diag => container.SuppressionChecker.IsSuppressed(diag, executionContext.SettingsKey))) - { - var suppressionDesc = SupportedSuppressions.Single(x => x.SuppressedDiagnosticId == diag.Id); - result.Add(Suppression.Create(suppressionDesc, diag)); - } - return result; - } - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs deleted file mode 100644 index 0357141106..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.g.cs +++ /dev/null @@ -1,563 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Immutable; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Generated class that returns SupportedSuppressions for all Sonar C# and VB.NET rules - /// - internal sealed class SupportedSuppressionsBuilder - { - private static readonly Lazy lazy = new Lazy(() => new SupportedSuppressionsBuilder()); - - public static SupportedSuppressionsBuilder Instance => lazy.Value; - - public ImmutableArray Descriptors { get; } - - private SupportedSuppressionsBuilder() - { - Descriptors = GetDescriptors(); - } - - private static ImmutableArray GetDescriptors() - { - var descriptors = new SuppressionDescriptor[] - { - // ************************************************************************************************* - // If the number of diagnostic ids changes significantly or decreases when the analysers are updated, - // investigate! There may be a problem with code generator. - // ************************************************************************************************* - // Number of unique diagnostic ids (C# and VB.NET): 504 - CreateDescriptor("S100"), - CreateDescriptor("S1006"), - CreateDescriptor("S101"), - CreateDescriptor("S103"), - CreateDescriptor("S104"), - CreateDescriptor("S1048"), - CreateDescriptor("S105"), - CreateDescriptor("S106"), - CreateDescriptor("S1066"), - CreateDescriptor("S1067"), - CreateDescriptor("S107"), - CreateDescriptor("S1075"), - CreateDescriptor("S108"), - CreateDescriptor("S109"), - CreateDescriptor("S110"), - CreateDescriptor("S1104"), - CreateDescriptor("S1109"), - CreateDescriptor("S1110"), - CreateDescriptor("S1116"), - CreateDescriptor("S1117"), - CreateDescriptor("S1118"), - CreateDescriptor("S112"), - CreateDescriptor("S1121"), - CreateDescriptor("S1123"), - CreateDescriptor("S1125"), - CreateDescriptor("S1128"), - CreateDescriptor("S113"), - CreateDescriptor("S1133"), - CreateDescriptor("S1134"), - CreateDescriptor("S1135"), - CreateDescriptor("S114"), - CreateDescriptor("S1144"), - CreateDescriptor("S1147"), - CreateDescriptor("S1151"), - CreateDescriptor("S1155"), - CreateDescriptor("S1163"), - CreateDescriptor("S1168"), - CreateDescriptor("S117"), - CreateDescriptor("S1172"), - CreateDescriptor("S1185"), - CreateDescriptor("S1186"), - CreateDescriptor("S119"), - CreateDescriptor("S1192"), - CreateDescriptor("S1197"), - CreateDescriptor("S1199"), - CreateDescriptor("S1200"), - CreateDescriptor("S1206"), - CreateDescriptor("S121"), - CreateDescriptor("S1210"), - CreateDescriptor("S1215"), - CreateDescriptor("S122"), - CreateDescriptor("S1226"), - CreateDescriptor("S1227"), - CreateDescriptor("S1244"), - CreateDescriptor("S125"), - CreateDescriptor("S126"), - CreateDescriptor("S1264"), - CreateDescriptor("S127"), - CreateDescriptor("S1301"), - CreateDescriptor("S1309"), - CreateDescriptor("S131"), - CreateDescriptor("S1312"), - CreateDescriptor("S1313"), - CreateDescriptor("S134"), - CreateDescriptor("S138"), - CreateDescriptor("S139"), - CreateDescriptor("S1449"), - CreateDescriptor("S1450"), - CreateDescriptor("S1451"), - CreateDescriptor("S1479"), - CreateDescriptor("S1481"), - CreateDescriptor("S1541"), - CreateDescriptor("S1542"), - CreateDescriptor("S1607"), - CreateDescriptor("S1643"), - CreateDescriptor("S1645"), - CreateDescriptor("S1654"), - CreateDescriptor("S1656"), - CreateDescriptor("S1659"), - CreateDescriptor("S1694"), - CreateDescriptor("S1696"), - CreateDescriptor("S1698"), - CreateDescriptor("S1699"), - CreateDescriptor("S1751"), - CreateDescriptor("S1764"), - CreateDescriptor("S1821"), - CreateDescriptor("S1848"), - CreateDescriptor("S1854"), - CreateDescriptor("S1858"), - CreateDescriptor("S1862"), - CreateDescriptor("S1871"), - CreateDescriptor("S1905"), - CreateDescriptor("S1939"), - CreateDescriptor("S1940"), - CreateDescriptor("S1944"), - CreateDescriptor("S1994"), - CreateDescriptor("S2053"), - CreateDescriptor("S2068"), - CreateDescriptor("S2077"), - CreateDescriptor("S2092"), - CreateDescriptor("S2094"), - CreateDescriptor("S2114"), - CreateDescriptor("S2115"), - CreateDescriptor("S2123"), - CreateDescriptor("S2139"), - CreateDescriptor("S2148"), - CreateDescriptor("S2156"), - CreateDescriptor("S2166"), - CreateDescriptor("S2178"), - CreateDescriptor("S2183"), - CreateDescriptor("S2184"), - CreateDescriptor("S2187"), - CreateDescriptor("S2190"), - CreateDescriptor("S2197"), - CreateDescriptor("S2198"), - CreateDescriptor("S2201"), - CreateDescriptor("S2219"), - CreateDescriptor("S2221"), - CreateDescriptor("S2222"), - CreateDescriptor("S2223"), - CreateDescriptor("S2225"), - CreateDescriptor("S2234"), - CreateDescriptor("S2245"), - CreateDescriptor("S2251"), - CreateDescriptor("S2252"), - CreateDescriptor("S2257"), - CreateDescriptor("S2259"), - CreateDescriptor("S2275"), - CreateDescriptor("S2290"), - CreateDescriptor("S2291"), - CreateDescriptor("S2292"), - CreateDescriptor("S2302"), - CreateDescriptor("S2304"), - CreateDescriptor("S2306"), - CreateDescriptor("S2325"), - CreateDescriptor("S2326"), - CreateDescriptor("S2327"), - CreateDescriptor("S2328"), - CreateDescriptor("S2330"), - CreateDescriptor("S2333"), - CreateDescriptor("S2339"), - CreateDescriptor("S2340"), - CreateDescriptor("S2342"), - CreateDescriptor("S2343"), - CreateDescriptor("S2344"), - CreateDescriptor("S2345"), - CreateDescriptor("S2346"), - CreateDescriptor("S2347"), - CreateDescriptor("S2348"), - CreateDescriptor("S2349"), - CreateDescriptor("S2352"), - CreateDescriptor("S2354"), - CreateDescriptor("S2355"), - CreateDescriptor("S2357"), - CreateDescriptor("S2358"), - CreateDescriptor("S2359"), - CreateDescriptor("S2360"), - CreateDescriptor("S2362"), - CreateDescriptor("S2363"), - CreateDescriptor("S2364"), - CreateDescriptor("S2365"), - CreateDescriptor("S2366"), - CreateDescriptor("S2367"), - CreateDescriptor("S2368"), - CreateDescriptor("S2369"), - CreateDescriptor("S2370"), - CreateDescriptor("S2372"), - CreateDescriptor("S2373"), - CreateDescriptor("S2374"), - CreateDescriptor("S2375"), - CreateDescriptor("S2376"), - CreateDescriptor("S2386"), - CreateDescriptor("S2387"), - CreateDescriptor("S2429"), - CreateDescriptor("S2436"), - CreateDescriptor("S2437"), - CreateDescriptor("S2445"), - CreateDescriptor("S2479"), - CreateDescriptor("S2486"), - CreateDescriptor("S2551"), - CreateDescriptor("S2583"), - CreateDescriptor("S2589"), - CreateDescriptor("S2612"), - CreateDescriptor("S2629"), - CreateDescriptor("S2674"), - CreateDescriptor("S2681"), - CreateDescriptor("S2688"), - CreateDescriptor("S2692"), - CreateDescriptor("S2696"), - CreateDescriptor("S2699"), - CreateDescriptor("S2701"), - CreateDescriptor("S2737"), - CreateDescriptor("S2743"), - CreateDescriptor("S2755"), - CreateDescriptor("S2757"), - CreateDescriptor("S2760"), - CreateDescriptor("S2761"), - CreateDescriptor("S2857"), - CreateDescriptor("S2925"), - CreateDescriptor("S2930"), - CreateDescriptor("S2931"), - CreateDescriptor("S2933"), - CreateDescriptor("S2934"), - CreateDescriptor("S2951"), - CreateDescriptor("S2952"), - CreateDescriptor("S2953"), - CreateDescriptor("S2955"), - CreateDescriptor("S2970"), - CreateDescriptor("S2971"), - CreateDescriptor("S2995"), - CreateDescriptor("S2996"), - CreateDescriptor("S2997"), - CreateDescriptor("S3005"), - CreateDescriptor("S3010"), - CreateDescriptor("S3011"), - CreateDescriptor("S3052"), - CreateDescriptor("S3059"), - CreateDescriptor("S3060"), - CreateDescriptor("S3063"), - CreateDescriptor("S3168"), - CreateDescriptor("S3169"), - CreateDescriptor("S3172"), - CreateDescriptor("S3215"), - CreateDescriptor("S3216"), - CreateDescriptor("S3217"), - CreateDescriptor("S3218"), - CreateDescriptor("S3220"), - CreateDescriptor("S3234"), - CreateDescriptor("S3235"), - CreateDescriptor("S3236"), - CreateDescriptor("S3237"), - CreateDescriptor("S3240"), - CreateDescriptor("S3241"), - CreateDescriptor("S3242"), - CreateDescriptor("S3244"), - CreateDescriptor("S3246"), - CreateDescriptor("S3247"), - CreateDescriptor("S3249"), - CreateDescriptor("S3251"), - CreateDescriptor("S3253"), - CreateDescriptor("S3254"), - CreateDescriptor("S3256"), - CreateDescriptor("S3257"), - CreateDescriptor("S3260"), - CreateDescriptor("S3261"), - CreateDescriptor("S3262"), - CreateDescriptor("S3263"), - CreateDescriptor("S3264"), - CreateDescriptor("S3265"), - CreateDescriptor("S3267"), - CreateDescriptor("S3329"), - CreateDescriptor("S3330"), - CreateDescriptor("S3343"), - CreateDescriptor("S3346"), - CreateDescriptor("S3353"), - CreateDescriptor("S3358"), - CreateDescriptor("S3363"), - CreateDescriptor("S3366"), - CreateDescriptor("S3376"), - CreateDescriptor("S3385"), - CreateDescriptor("S3397"), - CreateDescriptor("S3398"), - CreateDescriptor("S3400"), - CreateDescriptor("S3415"), - CreateDescriptor("S3416"), - CreateDescriptor("S3427"), - CreateDescriptor("S3431"), - CreateDescriptor("S3433"), - CreateDescriptor("S3440"), - CreateDescriptor("S3441"), - CreateDescriptor("S3442"), - CreateDescriptor("S3443"), - CreateDescriptor("S3444"), - CreateDescriptor("S3445"), - CreateDescriptor("S3447"), - CreateDescriptor("S3449"), - CreateDescriptor("S3450"), - CreateDescriptor("S3451"), - CreateDescriptor("S3453"), - CreateDescriptor("S3456"), - CreateDescriptor("S3457"), - CreateDescriptor("S3458"), - CreateDescriptor("S3459"), - CreateDescriptor("S3464"), - CreateDescriptor("S3466"), - CreateDescriptor("S3532"), - CreateDescriptor("S3597"), - CreateDescriptor("S3598"), - CreateDescriptor("S3600"), - CreateDescriptor("S3603"), - CreateDescriptor("S3604"), - CreateDescriptor("S3610"), - CreateDescriptor("S3626"), - CreateDescriptor("S3655"), - CreateDescriptor("S3717"), - CreateDescriptor("S3776"), - CreateDescriptor("S3860"), - CreateDescriptor("S3866"), - CreateDescriptor("S3869"), - CreateDescriptor("S3871"), - CreateDescriptor("S3872"), - CreateDescriptor("S3874"), - CreateDescriptor("S3875"), - CreateDescriptor("S3876"), - CreateDescriptor("S3877"), - CreateDescriptor("S3878"), - CreateDescriptor("S3880"), - CreateDescriptor("S3881"), - CreateDescriptor("S3884"), - CreateDescriptor("S3885"), - CreateDescriptor("S3887"), - CreateDescriptor("S3889"), - CreateDescriptor("S3897"), - CreateDescriptor("S3898"), - CreateDescriptor("S3900"), - CreateDescriptor("S3902"), - CreateDescriptor("S3903"), - CreateDescriptor("S3904"), - CreateDescriptor("S3906"), - CreateDescriptor("S3908"), - CreateDescriptor("S3909"), - CreateDescriptor("S3923"), - CreateDescriptor("S3925"), - CreateDescriptor("S3926"), - CreateDescriptor("S3927"), - CreateDescriptor("S3928"), - CreateDescriptor("S3937"), - CreateDescriptor("S3949"), - CreateDescriptor("S3956"), - CreateDescriptor("S3962"), - CreateDescriptor("S3963"), - CreateDescriptor("S3966"), - CreateDescriptor("S3967"), - CreateDescriptor("S3971"), - CreateDescriptor("S3972"), - CreateDescriptor("S3973"), - CreateDescriptor("S3981"), - CreateDescriptor("S3984"), - CreateDescriptor("S3990"), - CreateDescriptor("S3992"), - CreateDescriptor("S3993"), - CreateDescriptor("S3994"), - CreateDescriptor("S3995"), - CreateDescriptor("S3996"), - CreateDescriptor("S3997"), - CreateDescriptor("S3998"), - CreateDescriptor("S4000"), - CreateDescriptor("S4002"), - CreateDescriptor("S4004"), - CreateDescriptor("S4005"), - CreateDescriptor("S4015"), - CreateDescriptor("S4016"), - CreateDescriptor("S4017"), - CreateDescriptor("S4018"), - CreateDescriptor("S4019"), - CreateDescriptor("S4022"), - CreateDescriptor("S4023"), - CreateDescriptor("S4025"), - CreateDescriptor("S4026"), - CreateDescriptor("S4027"), - CreateDescriptor("S4035"), - CreateDescriptor("S4036"), - CreateDescriptor("S4039"), - CreateDescriptor("S4040"), - CreateDescriptor("S4041"), - CreateDescriptor("S4047"), - CreateDescriptor("S4049"), - CreateDescriptor("S4050"), - CreateDescriptor("S4052"), - CreateDescriptor("S4055"), - CreateDescriptor("S4056"), - CreateDescriptor("S4057"), - CreateDescriptor("S4058"), - CreateDescriptor("S4059"), - CreateDescriptor("S4060"), - CreateDescriptor("S4061"), - CreateDescriptor("S4069"), - CreateDescriptor("S4070"), - CreateDescriptor("S4136"), - CreateDescriptor("S4143"), - CreateDescriptor("S4144"), - CreateDescriptor("S4158"), - CreateDescriptor("S4159"), - CreateDescriptor("S4200"), - CreateDescriptor("S4201"), - CreateDescriptor("S4210"), - CreateDescriptor("S4211"), - CreateDescriptor("S4212"), - CreateDescriptor("S4214"), - CreateDescriptor("S4220"), - CreateDescriptor("S4225"), - CreateDescriptor("S4226"), - CreateDescriptor("S4260"), - CreateDescriptor("S4261"), - CreateDescriptor("S4275"), - CreateDescriptor("S4277"), - CreateDescriptor("S4347"), - CreateDescriptor("S4423"), - CreateDescriptor("S4426"), - CreateDescriptor("S4428"), - CreateDescriptor("S4433"), - CreateDescriptor("S4456"), - CreateDescriptor("S4457"), - CreateDescriptor("S4462"), - CreateDescriptor("S4487"), - CreateDescriptor("S4502"), - CreateDescriptor("S4507"), - CreateDescriptor("S4524"), - CreateDescriptor("S4545"), - CreateDescriptor("S4581"), - CreateDescriptor("S4583"), - CreateDescriptor("S4586"), - CreateDescriptor("S4635"), - CreateDescriptor("S4663"), - CreateDescriptor("S4790"), - CreateDescriptor("S4792"), - CreateDescriptor("S4830"), - CreateDescriptor("S5034"), - CreateDescriptor("S5042"), - CreateDescriptor("S5122"), - CreateDescriptor("S5332"), - CreateDescriptor("S5344"), - CreateDescriptor("S5443"), - CreateDescriptor("S5445"), - CreateDescriptor("S5542"), - CreateDescriptor("S5547"), - CreateDescriptor("S5659"), - CreateDescriptor("S5693"), - CreateDescriptor("S5753"), - CreateDescriptor("S5766"), - CreateDescriptor("S5773"), - CreateDescriptor("S5856"), - CreateDescriptor("S5944"), - CreateDescriptor("S6145"), - CreateDescriptor("S6146"), - CreateDescriptor("S6354"), - CreateDescriptor("S6377"), - CreateDescriptor("S6418"), - CreateDescriptor("S6419"), - CreateDescriptor("S6420"), - CreateDescriptor("S6421"), - CreateDescriptor("S6422"), - CreateDescriptor("S6423"), - CreateDescriptor("S6424"), - CreateDescriptor("S6444"), - CreateDescriptor("S6507"), - CreateDescriptor("S6513"), - CreateDescriptor("S6561"), - CreateDescriptor("S6562"), - CreateDescriptor("S6563"), - CreateDescriptor("S6566"), - CreateDescriptor("S6575"), - CreateDescriptor("S6580"), - CreateDescriptor("S6585"), - CreateDescriptor("S6588"), - CreateDescriptor("S6602"), - CreateDescriptor("S6603"), - CreateDescriptor("S6605"), - CreateDescriptor("S6607"), - CreateDescriptor("S6608"), - CreateDescriptor("S6609"), - CreateDescriptor("S6610"), - CreateDescriptor("S6612"), - CreateDescriptor("S6613"), - CreateDescriptor("S6617"), - CreateDescriptor("S6618"), - CreateDescriptor("S6640"), - CreateDescriptor("S6664"), - CreateDescriptor("S6667"), - CreateDescriptor("S6668"), - CreateDescriptor("S6669"), - CreateDescriptor("S6670"), - CreateDescriptor("S6672"), - CreateDescriptor("S6673"), - CreateDescriptor("S6674"), - CreateDescriptor("S6675"), - CreateDescriptor("S6677"), - CreateDescriptor("S6678"), - CreateDescriptor("S6781"), - CreateDescriptor("S6797"), - CreateDescriptor("S6798"), - CreateDescriptor("S6800"), - CreateDescriptor("S6802"), - CreateDescriptor("S6803"), - CreateDescriptor("S6930"), - CreateDescriptor("S6931"), - CreateDescriptor("S6932"), - CreateDescriptor("S6934"), - CreateDescriptor("S6960"), - CreateDescriptor("S6961"), - CreateDescriptor("S6962"), - CreateDescriptor("S6964"), - CreateDescriptor("S6965"), - CreateDescriptor("S6966"), - CreateDescriptor("S6967"), - CreateDescriptor("S6968"), - CreateDescriptor("S7039"), - CreateDescriptor("S7130"), - CreateDescriptor("S7131"), - CreateDescriptor("S7133"), - CreateDescriptor("S818"), - CreateDescriptor("S881"), - CreateDescriptor("S907"), - CreateDescriptor("S927"), - }; - return ImmutableArray.ToImmutableArray(descriptors); - } - - private static SuppressionDescriptor CreateDescriptor(string diagId) => - new SuppressionDescriptor("X" + diagId, diagId, "Suppressed on the Sonar server"); - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt b/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt deleted file mode 100644 index fac98c2b0d..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SupportedSuppressionBuilder.tt +++ /dev/null @@ -1,194 +0,0 @@ -<#@ template debug="false" hostspecific="false" language="C#" #> -<#@ output extension=".g.cs" #> -<#@ assembly name="System.Core" #> -<#@ assembly name="$(SonarCSharpDllPath)" #> -<#@ assembly name="$(SonarVBDllPath)" #> -<#@ assembly name="$(EnterpriseSonarCSharpDllPath)" #> -<#@ assembly name="$(EnterpriseSonarVBDllPath)" #> -<#@ assembly name="$(MSCodeAnalysisCommonDllPath)" #> -<#@ assembly name="$(MSCodeAnalysisWorkspacesDllPath)" #> -<#@ assembly name="$(SystemCompositionAttributedModelDllPath)" #> -<#@ assembly name="netstandard" #> -<#@ assembly name="System.Runtime" #> -<#@ import namespace="System.Linq" #> -<#@ import namespace="System.Reflection" #> -<#@ import namespace="System.Collections.Generic" #> -<#@ import namespace="SonarAnalyzer" #> -<#@ import namespace="Microsoft.CodeAnalysis" #> -<#@ import namespace="Microsoft.CodeAnalysis.Diagnostics" #> -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Immutable; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - /// - /// Generated class that returns SupportedSuppressions for all Sonar C# and VB.NET rules - /// - internal sealed class SupportedSuppressionsBuilder - { - private static readonly Lazy lazy = new Lazy(() => new SupportedSuppressionsBuilder()); - - public static SupportedSuppressionsBuilder Instance => lazy.Value; - - public ImmutableArray Descriptors { get; } - - private SupportedSuppressionsBuilder() - { - Descriptors = GetDescriptors(); - } - - private static ImmutableArray GetDescriptors() - { - var descriptors = new SuppressionDescriptor[] - { - // ************************************************************************************************* - // If the number of diagnostic ids changes significantly or decreases when the analysers are updated, - // investigate! There may be a problem with code generator. - // ************************************************************************************************* -<# - System.AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; - - var idList = GetFilteredDiagnosticIds(); - WriteLine(" // Number of unique diagnostic ids (C# and VB.NET): " + idList.Count()); - foreach(var id in idList) - { -#> - CreateDescriptor("<#= id #>"), -<# - } -#> - }; - return ImmutableArray.ToImmutableArray(descriptors); - } - - private static SuppressionDescriptor CreateDescriptor(string diagId) => - new SuppressionDescriptor("X" + diagId, diagId, "Suppressed on the Sonar server"); - } -} -<#+ - -// T4 Text Templates -// MS docs - see https://docs.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2022 -// -// Notes on debugging through the generator -// ---------------------------------------- -// * change the "debug" value in line 1 to "true" -// * add a call to "System.Diagnostics.Debugger.Launch();" somewhere in the code below -// * save this file. -// A dialogue should pop up asking you attach a debugger. -// -// -// Notes on resolving Roslyn assemblies -// ------------------------------------ -// The T4 transformation can be done inside VS (by saving the .tt file, or right-clicking on it in the Solution Explorer -// and selecting "Run Custom Tool"). When run that way, the transformation is hosted in-memory in VS, and it uses its -// versions of the Roslyn assemblies. It also uses the binding redirects in devenv.exe.config to forward references from -// older versions of Roslyn to the VS versions. -// -// When the transformation is done at build time (whether building in VS or outside), the transformation is hosted by -// MSBuild, which locates and resolves the assemblies separately. -// To make the transformation work in MSBuild, we need to: -// 1) reference some Roslyn NuGet packages required during the transformation, -// 2) handling the binding redirects in code (using the AssemblyResolve event), and -// 3) use Reflection to create the analyzer instances and extract the analyzer ids. - - private System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) - { - // Simple binding redirect - try a simple match based on name, ignoring the version. - // This approximates to what VS is doing when redirecting the Roslyn assemblies. - var requiredAsm = new System.Reflection.AssemblyName(args.Name); - return System.AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == requiredAsm.Name); - } - - // The set of assemblies in which we should look for diagnostics - private readonly Assembly[] AssembliesWithDiagnostics = new Assembly[] - { - typeof(SonarAnalyzer.CSharp.Rules.CommentKeyword).Assembly, - typeof(SonarAnalyzer.Enterprise.CSharp.Rules.SymbolicExecutionRunner).Assembly, - typeof(SonarAnalyzer.VisualBasic.Rules.CommentKeyword).Assembly, - typeof(SonarAnalyzer.Enterprise.VisualBasic.Rules.SymbolicExecutionRunner).Assembly - }; - - private IEnumerable GetAllDiagnosticIds() => - AssembliesWithDiagnostics.SelectMany(asm => asm.GetTypes()) - .SelectMany(type => GetIdsUsingReflection(type)); - private IEnumerable GetFilteredDiagnosticIds() - { - return GetAllDiagnosticIds() - .Distinct() - .Where(id => !id.StartsWith("S999")) // exclude "utility" analyzers - .OrderBy(x => x); - } - - private IEnumerable GetIdsUsingReflection(Type t) - { - try - { - if (IsDiagnosticAnalyzer(t)) - { - var analyzer = Create(t); - return GetDiagnosticIds(analyzer); - } - } - catch(Exception e) - { - WriteLine($"[ERROR] Error creating diagnostic. {t.FullName}, Error: {e.Message}"); - } - return Array.Empty(); - } - - private static object Create(Type t) => - Activator.CreateInstance(t); - - private bool IsDiagnosticAnalyzer(Type t) - { - if (t.IsAbstract) { return false; } - return InheritsFromDiagnosticAnalyzer(t) && t.GetCustomAttributes(true).Any(x => x.GetType().Name == "DiagnosticAnalyzerAttribute"); - } - - private static bool InheritsFromDiagnosticAnalyzer(Type t) - { - // Note: there are some symbolic execution classes that have the [DiagnosticAnalyzer] attribute - // but that do not inherit from DiagnosticAnalyzer - // e.g. https://github.com/SonarSource/sonar-dotnet/blob/aab23c510aa525565c01658418a4c7affa29d17a/analyzers/src/SonarAnalyzer.CSharp/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.cs#L23 - // See bug https://github.com/SonarSource/sonarlint-visualstudio/issues/4467 - if (t == null) { return false; } - if (t.FullName == "Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer") { return true; } - return InheritsFromDiagnosticAnalyzer(t.BaseType); - } - - private IEnumerable GetDiagnosticIds(object analyzerInstance) - { - var propertyInfo = analyzerInstance.GetType().GetProperty("SupportedDiagnostics"); - var supportedDiagnostics = propertyInfo.GetValue(analyzerInstance) as IEnumerable; - - return supportedDiagnostics.Select(GetIdFromDescriptor); - } - - private string GetIdFromDescriptor(object diagnosticDescriptor) - { - var propertyInfo = diagnosticDescriptor.GetType().GetProperty("Id"); - return propertyInfo.GetValue(diagnosticDescriptor) as string; - } -#> \ No newline at end of file diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs deleted file mode 100644 index 0774940846..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionChecker.cs +++ /dev/null @@ -1,142 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using Microsoft.CodeAnalysis; -using SonarLint.VisualStudio.Core.Helpers; -using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; -using SonarQube.Client; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - internal interface ISuppressionChecker - { - /// - /// Returns true if the diagnostic should be suppressed, otherwise false - /// - bool IsSuppressed(Diagnostic reportedDiagnostic, string settingsKey); - } - - internal class SuppressionChecker : ISuppressionChecker - { - private readonly ISettingsCache settingsCache; - private readonly IChecksumCalculator checksumCalculator; - - public SuppressionChecker(ISettingsCache settingsCache) - : this(settingsCache, new ChecksumCalculator()) - { - } - - internal SuppressionChecker(ISettingsCache settingsCache, IChecksumCalculator checksumCalculator) - { - this.settingsCache = settingsCache; - this.checksumCalculator = checksumCalculator; - } - - public bool IsSuppressed(Diagnostic reportedDiagnostic, string settingsKey) - { - // Can't match issues that don't have a location in source - if (reportedDiagnostic.Location == null || reportedDiagnostic.Location.Kind != LocationKind.SourceFile) - { - return false; - } - - var settings = settingsCache.GetSettings(settingsKey); - var suppressedIssues = settings.Suppressions; - - if (suppressedIssues == null || !suppressedIssues.Any()) - { - return false; - } - - // TODO: consider perf. This initial implementation is very inefficient: - // * it loops through every issue - // * the hash of the Roslyn issue could be calculate multiple times. - bool matchFound = suppressedIssues.Any(x => IsMatch(reportedDiagnostic, x, checksumCalculator)); - return matchFound; - } - - internal static /* for testing */ bool IsMatch(Diagnostic diagnostic, SuppressedIssue suppressedIssue, IChecksumCalculator checksumCalculator) - { - // Criteria for matching a Roslyn issue to an issue from the server: - // (1) same issue key - // (2) same file - // (3) same line number OR same line hash (to take account of edits in the file since it was analysed) - - // Note: this first implementation is written in a verbose style to make it easier to debug. - - // (1) Id - if (!StringComparer.OrdinalIgnoreCase.Equals(diagnostic.Id, suppressedIssue.RoslynRuleId)) - { - return false; - } - - // (2) File - if (!IsSameFile(diagnostic, suppressedIssue)) - { - return false; - } - - // (3) Location - line matches - var roslynLineSpan = diagnostic.Location.GetLineSpan(); - if (IsSameLine(roslynLineSpan, suppressedIssue)) - { - return true; - } - - // (3) Location - hash (most expensive check) - var syntaxTree = diagnostic.Location.SourceTree; - // TODO: check why the existing RoslynLiveIssueFactory.Create is using the EndLine of the issue - // rather than the StartLine - var lineText = syntaxTree.GetText().Lines[roslynLineSpan.EndLinePosition.Line].ToString(); - var roslynHash = checksumCalculator.Calculate(lineText); - - if (StringComparer.Ordinal.Equals(roslynHash, suppressedIssue.Hash)) - { - return true; - } - - return false; - } - - private static bool IsSameFile(Diagnostic diagnostic, SuppressedIssue suppressedIssue) => - PathHelper.IsServerFileMatch(diagnostic.Location.SourceTree?.FilePath, suppressedIssue.FilePath); - - private static bool IsSameLine(FileLinePositionSpan roslynLineSpan, SuppressedIssue suppressedIssue) - { - // Special case: file-level issues - var roslynFileLevelIssue = IsRoslynFileLevelIssue(roslynLineSpan); - var sonarFileLevelIssue = !suppressedIssue.RoslynIssueLine.HasValue; - if (sonarFileLevelIssue || roslynFileLevelIssue) - { - // File-level issues can only match other file-level issues - return (sonarFileLevelIssue && roslynFileLevelIssue); - } - - return suppressedIssue.RoslynIssueLine.Value == roslynLineSpan.StartLinePosition.Line; - } - - private static bool IsRoslynFileLevelIssue(FileLinePositionSpan lineSpan) => lineSpan.StartLinePosition.Line == 0 && - lineSpan.StartLinePosition.Character == 0 && - lineSpan.EndLinePosition.Line == 0 && - lineSpan.EndLinePosition.Character == 0; - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs deleted file mode 100644 index 5809d6b542..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressionExecutionContext.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis.Diagnostics; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - public interface ISuppressionExecutionContext - { - bool IsInConnectedMode { get; } - string SettingsKey { get; } - } - internal class SuppressionExecutionContext : ISuppressionExecutionContext - { - private const string Exp = @"\\SonarLint for Visual Studio\\Bindings\\(?[^\\/]+)\\(CSharp|VB)\\SonarLint.xml$"; - private static readonly Regex SonarLintFileRegEx = new Regex(Exp, - RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant, - RegexConstants.DefaultTimeout); - - public SuppressionExecutionContext(AnalyzerOptions analyzerOptions) - { - GetProjectKey(analyzerOptions); - } - - private void GetProjectKey(AnalyzerOptions analyzerOptions) - { - foreach (var item in analyzerOptions.AdditionalFiles) - { - var match = SonarLintFileRegEx.Match(item.Path); - if (match.Success) - { - SettingsKey = match.Groups["solutionName"].Value; - return; - } - } - } - - public bool IsInConnectedMode => SettingsKey != null; - - public string SettingsKey { get; private set; } = null; - - public string Mode => IsInConnectedMode ? "Connected" : "Standalone"; - } -} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets b/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets deleted file mode 100644 index 9478e957a0..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/SuppressorCodeGen.targets +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.CSharp.dll - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.Enterprise.CSharp.dll - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.VisualBasic.dll - $(EnterpriseDotnetAnalyzersExtractDir)\SonarAnalyzer.Enterprise.VisualBasic.dll - - $(PkgMicrosoft_CodeAnalysis_Common)\lib\netstandard2.0\Microsoft.CodeAnalysis.dll - $(PkgMicrosoft_CodeAnalysis_Workspaces_Common)\lib\netstandard2.0\Microsoft.CodeAnalysis.Workspaces.dll - $(PkgSystem_Composition_AttributedModel)\lib\netstandard1.0\System.Composition.AttributedModel.dll - - - - false - true - - - - - - $(SonarCSharpDllPath) - false - - - $(SonarVBDllPath) - false - - - $(EnterpriseSonarCSharpDllPath) - false - - - $(EnterpriseSonarVBDllPath) - false - - - $(MSCodeAnalysisCommonDllPath) - false - - - $(MSCodeAnalysisWorkspacesDllPath) - false - - - $(SystemCompositionAttributedModelDllPath) - false - - - - - - TextTemplatingFileGenerator - SupportedSuppressionBuilder.g.cs - - - True - True - SupportedSuppressionBuilder.tt - - - - - - - - - diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json b/src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json deleted file mode 100644 index 17b4e630d2..0000000000 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/packages.lock.json +++ /dev/null @@ -1,1292 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v4.7.2": { - "Microsoft.CodeAnalysis.Common": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.2", - "System.Collections.Immutable": "5.0.0", - "System.Memory": "4.5.4", - "System.Reflection.Metadata": "5.0.0", - "System.Runtime.CompilerServices.Unsafe": "5.0.0", - "System.Text.Encoding.CodePages": "4.5.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", - "dependencies": { - "Humanizer.Core": "2.2.0", - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.CodeAnalysis.Common": "[3.11.0]", - "System.Composition": "1.0.31", - "System.IO.Pipelines": "5.0.1" - } - }, - "Microsoft.VisualStudio.SDK": { - "type": "Direct", - "requested": "[17.0.31902.203, )", - "resolved": "17.0.31902.203", - "contentHash": "YSOMLjLm0k4Q5JeoPvyRrYaHdUzxkKtkt9Hn/0NjWDdQ7tLrhZjR3SOiaSMYOFYQCQ0G7v2UBIRHfbGAnt7SNg==", - "dependencies": { - "Microsoft.VisualStudio.CommandBars": "17.0.31902.203", - "Microsoft.VisualStudio.ComponentModelHost": "17.0.487", - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Debugger.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.14.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.16.0": "17.0.31902.203", - "Microsoft.VisualStudio.Designer.Interfaces": "17.0.31902.203", - "Microsoft.VisualStudio.Editor": "17.0.487", - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.Language.Intellisense": "17.0.487", - "Microsoft.VisualStudio.Language.NavigateTo.Interfaces": "17.0.487", - "Microsoft.VisualStudio.Language.StandardClassification": "17.0.487", - "Microsoft.VisualStudio.LanguageServer.Client": "17.0.5158", - "Microsoft.VisualStudio.OLE.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Package.LanguageService.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31902.203", - "Microsoft.VisualStudio.Setup.Configuration.Interop": "3.0.4492", - "Microsoft.VisualStudio.Shell.Design": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.11.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.8.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Interop.9.0": "17.0.31902.203", - "Microsoft.VisualStudio.TaskRunnerExplorer.14.0": "14.0.0", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.TextManager.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.11.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.12.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.8.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextManager.Interop.9.0": "17.0.31902.203", - "Microsoft.VisualStudio.TextTemplating.VSHost": "17.0.31902.203", - "Microsoft.VisualStudio.VCProjectEngine": "17.0.31902.203", - "Microsoft.VisualStudio.VSHelp": "17.0.31902.203", - "Microsoft.VisualStudio.VSHelp80": "17.0.31902.203", - "Microsoft.VisualStudio.Validation": "17.0.28", - "Microsoft.VisualStudio.WCFReference.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Web.BrowserLink.12.0": "12.0.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.ComponentModel.Composition": "4.5.0", - "VSLangProj": "17.0.31902.203", - "VSLangProj100": "17.0.31902.203", - "VSLangProj110": "17.0.31902.203", - "VSLangProj140": "17.0.31902.203", - "VSLangProj150": "17.0.31902.203", - "VSLangProj157": "17.0.31902.203", - "VSLangProj158": "17.0.31902.203", - "VSLangProj165": "17.0.31902.203", - "VSLangProj2": "17.0.31902.203", - "VSLangProj80": "17.0.31902.203", - "VSLangProj90": "17.0.31902.203", - "envdte": "17.0.31902.203", - "envdte100": "17.0.31902.203", - "envdte80": "17.0.31902.203", - "envdte90": "17.0.31902.203", - "envdte90a": "17.0.31902.203", - "stdole": "17.0.31902.203" - } - }, - "Newtonsoft.Json": { - "type": "Direct", - "requested": "[13.0.3, )", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.Composition.AttributedModel": { - "type": "Direct", - "requested": "[1.0.31, )", - "resolved": "1.0.31", - "contentHash": "NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==" - }, - "System.IO.Abstractions": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "1h4krG51ZiW/CGzM8gtqrRW2oeG6WZDfPaj27suexL8PxBVahsUlUKMJrqI4kkh6ggHLSDd7MFeU8orpk6COZg==" - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.4.0", - "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" - }, - "envdte": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "AC6jYeSnDnYZEs5nHKEtBupRWAQxriX2X3M25HyJlU9cvCCqPCByMwIbvlz8kXk+GfGxSL8sN+YOihU2SvrjXw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte100": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "XICXfPHF4SQzqpUtQgXZsuSTyYOdSOymPMUqH/Q6QBrpEoMiJxmW/eEvLwgqJqiW5+HaajJImlVJkrnZJ4fjfg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "79xAQpQmKqNnBWtH96Fm+onXsCS7ZTK0CfhCIe3BC56whCMwyh52M/+Qj/xOGhbFnPpjEzJhPnoL1jppniyRAw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte90": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "AMFd0yjzXUV26i0Yzr5MBdolXtz92NZWUvacohGRFFHIHaa8BkKl1c40nw9m33l4cXcwMECsvPkhAcfietYkQg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "envdte90a": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "8rZmZEBu8uDMF1Fq1CITa540cJA8lVj9d9B2D2IgDL6heGkBfQc1nBf2BRcJT81SS9c0wEI1VLIVe3WSPeYG5g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==" - }, - "MessagePack": { - "type": "Transitive", - "resolved": "2.2.85", - "contentHash": "3SqAgwNV5LOf+ZapHmjQMUc7WDy/1ur9CfFNjgnfMZKCB5CxkVVbyHa06fObjGTEHZI7mcDathYjkI+ncr92ZQ==", - "dependencies": { - "MessagePack.Annotations": "2.2.85", - "Microsoft.Bcl.AsyncInterfaces": "1.0.0", - "System.Collections.Immutable": "1.5.0", - "System.Memory": "4.5.3", - "System.Reflection.Emit": "4.6.0", - "System.Reflection.Emit.Lightweight": "4.6.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2", - "System.Threading.Tasks.Extensions": "4.5.3" - } - }, - "MessagePack.Annotations": { - "type": "Transitive", - "resolved": "2.2.85", - "contentHash": "YptRsDCQK35K5FhmZ0LojW4t8I6DpetLfK5KG8PVY2f6h7/gdyr8f4++xdSEK/xS6XX7/GPvEpqszKVPksCsiQ==" - }, - "Microsoft.Alm.Authentication": { - "type": "Transitive", - "resolved": "4.0.0.1", - "contentHash": "G+EwnnZeoee3McrWZQ0TOtQShJyCzFwhJdEXIbz5t+G2e67KICipAHfflDSYwokB43YK+y5J7ArnRHDLYtNliA==", - "dependencies": { - "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.8" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "16.5.0", - "contentHash": "K0hfdWy+0p8DJXxzpNc4T5zHm4hf9QONAvyzvw3utKExmxRBShtV/+uHVYTblZWk+rIHNEHeglyXMmqfSshdFA==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.2", - "contentHash": "7xt6zTlIEizUgEsYAIgm37EbdkiMmr6fP6J9pDoKEpiGM4pi32BCPGr/IczmSJI9Zzp0a6HOzpr9OvpMP+2veA==" - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.IdentityModel.Clients.ActiveDirectory": { - "type": "Transitive", - "resolved": "3.13.8", - "contentHash": "RAYmpEVVnl8qWdPmm62k9wm6MUB9H4Gk3l7uBADDCCQJDOkpTAINzgWHTBF8hkaTHFLaGIfgOeeLcKT+K1PG7A==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" - }, - "Microsoft.ServiceHub.Analyzers": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "LQsmEP/5i9PvM6O1dx69Yj3C0z/tSWiaLjoX31jQ+ilJZ8x7yqthYOnWaQpeZKxJn+oFxymzGtXgPasnqYM/ww==" - }, - "Microsoft.ServiceHub.Client": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "hYqQlgUhnTq7VHYfIBvuWCwAiTjqhCfEX7d/ISVtEGEv7/N89QAbL+0XCz2NZRN6yMDtVMEoee5Q4k6/uwWlJg==", - "dependencies": { - "Microsoft.ServiceHub.Framework": "3.0.3078", - "Microsoft.ServiceHub.Resources": "3.0.3078", - "Microsoft.VisualStudio.RemoteControl": "16.3.32", - "Microsoft.VisualStudio.Telemetry": "16.3.176", - "StreamJsonRpc": "2.7.70", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.ServiceHub.Framework": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "RMBx+TEE3Fl6CRd1d1ZWKnNPRbPL23NFydDEEjRtZdwTSWe1x0gkUqnGU/ZgtqSsgWUfaQtEPxd8S9qfPGkz0Q==", - "dependencies": { - "Microsoft.ServiceHub.Analyzers": "3.0.3078", - "StreamJsonRpc": "2.7.70", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.ServiceHub.Resources": { - "type": "Transitive", - "resolved": "3.0.3078", - "contentHash": "02mGIKyVfnXFEeicpV2RbZapHd6vcefFSSZvjAA+O0kWgB9x2D5Pd3M94Il9LiLgFnw3mmxtf68tbEjOhQ0rWg==" - }, - "Microsoft.VisualStudio.CommandBars": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "+eBuWROC04s6NyVhJW/GJIqy8gOhFaxkWiHtuEf2bVTGkKQ/gx/Rjfegj79H/xEPnBmvbKp8e5+dxJ1WA4r1XA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.ComponentModelHost": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "mIvfzFYYtPbf+aZRLjtOmMfksKPGML2U2z7RqHJ+m0AVTumssJbOpaD5gOgYcUvE6+AjFoUpg+NLVjwxdZnVYQ==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.CoreUtility": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "qk4BeMGWIklu4ia8zf6JPLUGXuJcbjhqfYhIHYbDKETmGxRHoNsvzGBIZT8GkS4VSjcibOFnbCowJfNWy5TfhQ==", - "dependencies": { - "Microsoft.VisualStudio.Threading": "17.0.63", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "YipUF2uyw4MH3WFZf+/4lC9jcHjkL8GgPdwvH800x+kVHAAVIEXJTqPTmsJnBrT/QqXdPHQrM6Srae0VQjG3gA==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "NENWpbG7TvdF1TwesraECA+PgprCLR3Mfts7t8fL5KGc59QiRCeZTpVWBzotvRLMQ0lhA0VYYh1ErDV9D+LM2g==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "Os4TTwz9mrtS5AzTKMyEr2NPIJmmBGlf+Cn4eFPn6t4yMfDYfaFRnOtDgpiF7IyduB+mywO8v+wMLWiQuJth9Q==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.InteropA": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.14.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "UwQukQ2/HdIxKqHGDQ8oipxrJ4IWLdQfBBuY8Yl61SAVhcGzDP4Z/XkB7RSIbJOAeFDzu/kuKZ3zXR7jXazAJA==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "P7C071a0dx7TP9NEBmt8pR3BZMOIqQYNWYW1pvxaY6jOb99/q7sm/KgYDKGrK/HEBefbn6g9T6hrVlUeP2GsWQ==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.10.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.Interop.16.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "eYE5pOBbWacuf0pUyQHJH9LuP15x67CUy8VJv9AeJM3E91Y95Ff5hkCasBZtf9TJ9uMVnx0+6PdZLkZdYcS4Yg==", - "dependencies": { - "Microsoft.VisualStudio.Debugger.Interop.10.0": "17.0.31902.203", - "Microsoft.VisualStudio.Debugger.Interop.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Debugger.InteropA": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "y1CLio4/zI5PYWt760mJhduq/gpNZNrDwX2c2kBB5p+D8a1ExH4d2tNDTqbQdciOiNR47YenAcSUpyfHv9pNkg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Designer.Interfaces": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "eeNZCUX6RJGnc6YC6Bfs8qbEVt7WxWRPXEbEBjyOqCDU4dJPkR1OLVWMUHkA4XM3sXKImv1LmAwHZk3cWK5iGQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Editor": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "UuIMD8xLAkvwuszD7jlc4ADRckr59eyJ7YJFepTtja2IDMzt5SAGa0kxxjUQ3Zeg0KuhuwcZBwuYmSCgx5YSjA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.GraphModel": "17.0.31723.112", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31723.112", - "Microsoft.VisualStudio.RpcContracts": "17.0.51", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "Microsoft.VisualStudio.Text.UI.Wpf": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.63", - "Microsoft.VisualStudio.Validation": "17.0.28" - } - }, - "Microsoft.VisualStudio.GraphModel": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "53cv+WBrXiX3Bomk2W+gz74tFqFa54OvJ++u4RYAdntvXb/hx+m1EhBgtHKOFu057rVxAq/YNFY7vLCUusQGoQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.ImageCatalog": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "nNfMuMiuC0UX/r5ryjIuqWyj/Wzrz63wuEcX70NtICUNmm/Bc5blXmgH7Et+ArVzZlXi2ExNqbLAi60IpNdaYw==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Imaging": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "NCJX5aUZxcj32Al5E9PI2XONEjR8gQyzjGL2NfNxw8PzDPd6yMmU96y7rnvv0dclPUw0gCcVMtLnNwpCaeXBdg==", - "dependencies": { - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Threading": "17.0.64", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203", - "System.Collections.Immutable": "5.0.0" - } - }, - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "d9I8RzOeF9z1L/QJ3nWOMvJphsxe31cU7WBZW7aM0VTWZUWcYwYQH0CZWFPZsXgFWWRgiK5WL7FiMnYJfeDutw==" - }, - "Microsoft.VisualStudio.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KWBjmU85KpWJiqRgkvuJfQgW8/1RtkFnJ4e4EIcoXYHab/1tinXv219midxHJoc6Sa6E/7Uo3Ku4bWBQMFE9dA==" - }, - "Microsoft.VisualStudio.Language": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "j8hL3Wwst8hwstbBzY24WV0PWYkcWDAC/Y2JHdO9jdlBjvHCHUPN4zNtu6A9oCTNsyLMxzK+pq+X45FFVTeqrA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "Newtonsoft.Json": "13.0.1", - "StreamJsonRpc": "2.8.28", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0", - "System.Private.Uri": "4.3.2" - } - }, - "Microsoft.VisualStudio.Language.Intellisense": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "iPi01Ep2YM4rgL6z26s4eEy8hOYwt5jE1aLebknCnBzUAB3avrSVa1AcOo726SdVN8h/h0lVRE4LMrC4WAQ4rQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Language": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "Microsoft.VisualStudio.Language.NavigateTo.Interfaces": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "5/3AstEbNcaZtwwONLcHXc2Nzels7j89jzXsYsxPQCSW9inbDbQoBC6+wtsAwKxOu1BeKpPaGi7bmc7Y+R+vSA==", - "dependencies": { - "Microsoft.VisualStudio.Imaging": "17.0.31723.112", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Interop": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Logic": "17.0.448", - "Microsoft.VisualStudio.Utilities": "17.0.31723.112" - } - }, - "Microsoft.VisualStudio.Language.StandardClassification": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "n7l7tLV69+FtdH+/SPR8MdVc3UsAvlrgE38mMlXtW3dkxx5BYnSiHl93vZ4omDbX45Rzu9b4DDR1HMzg3GaKzQ==", - "dependencies": { - "Microsoft.VisualStudio.Text.Logic": "17.0.487" - } - }, - "Microsoft.VisualStudio.LanguageServer.Client": { - "type": "Transitive", - "resolved": "17.0.5158", - "contentHash": "cZNQHkvjpCnEnxZrLBflAAXc8CwJYMUgDEcCTgrY86Zpa6tHfaHpMoK3aBXtBlzlJ25vjeqBoilXbbCrPrHnJg==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.448", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31723.112", - "Microsoft.VisualStudio.Utilities": "17.0.31723.112", - "Microsoft.VisualStudio.Validation": "17.0.28", - "StreamJsonRpc": "2.8.28" - } - }, - "Microsoft.VisualStudio.OLE.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "lhZX3Vxm+bRP7/lb8zYG2DlLx3Ea1tnatV+SUYAJ+sL8Qr7v8VBmZurPxGsKYh5Q7ePNjOlkWk2eSaK6TaxUyg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Package.LanguageService.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ZR8l9shqIHXGzfd4Wx9H7Bkgd4ydsMC+Sh8wk05jOV3qS9j9cYTNkRKXOny9rK7lZJIQcH8fPYpIe48kAwZPmg==", - "dependencies": { - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.ProjectAggregator": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "C96eQGjS87DX1yjl/C57YFpWX2hD2Vi0BlLnz44nFhAxCvQ2Fy5FN/E63yNMIdX+iAZkCoG80c1D7C6fBOL0XA==" - }, - "Microsoft.VisualStudio.RemoteControl": { - "type": "Transitive", - "resolved": "16.3.41", - "contentHash": "Q9lz2anDPJxDLznQRaybv21aY3qgQJmGJiUonH8z2D0XAgKMlMelsu9bg9zhnKCxtA/jreRAM3Md2W6thiDOwQ==", - "dependencies": { - "Microsoft.VisualStudio.Utilities.Internal": "16.3.23" - } - }, - "Microsoft.VisualStudio.RpcContracts": { - "type": "Transitive", - "resolved": "17.0.51", - "contentHash": "4cMhOmJJl18BU+LTrcjNTLDqWBuCN5t87fB64n7UNyKLs03o4UVNKEjsUwlo6USbccGfoyba4mWPWIo7vL0qkA==", - "dependencies": { - "Microsoft.ServiceHub.Framework": "3.0.2061", - "StreamJsonRpc": "2.8.28" - } - }, - "Microsoft.VisualStudio.Setup.Configuration.Interop": { - "type": "Transitive", - "resolved": "3.0.4492", - "contentHash": "BfkqM96P8+N+cz4T+pxKrIKk2ZD1YMxCXH2ivtBDj5tx6Mc2YQLK1+3h+C6Qebper0RBipuHVn51lb9SZH6bKQ==" - }, - "Microsoft.VisualStudio.Shell.15.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "9bOKENGcO474GIgVoRzmkce6FdW/fiH/W2M0ULyRkFjNh9yEdJheYeyJFBr3Pb0EAwPwu2Q3gayQB7I9oTd/SA==", - "dependencies": { - "Microsoft.VisualStudio.ComponentModelHost": "17.0.487", - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.ProjectAggregator": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Design": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "gFVdvJ9HOOnq4ntBCcULraptK+V+Mu1GahUXmd0dhsomGzkzgqDqN7aYQ3ys+K45pJhNeB3MJ/NNQUI4woyiBQ==", - "dependencies": { - "Microsoft.VisualStudio.ImageCatalog": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.15.0": "17.0.31902.203", - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Framework": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "L9s0/zSMeNlh1Pyzh9vX7T2+O3vg0JxTgXHyXGP/36DpJh4f+87D4rj+/nkESY4YXIUKPVl7XWXh3NLG0yjj1w==", - "dependencies": { - "Microsoft.Build.Framework": "16.5.0", - "Microsoft.VisualStudio.GraphModel": "17.0.31902.203", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31902.203", - "Microsoft.VisualStudio.Interop": "17.0.31902.203", - "Microsoft.VisualStudio.Telemetry": "16.3.250", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.64", - "Microsoft.VisualStudio.Utilities": "17.0.31902.203", - "Newtonsoft.Json": "13.0.1", - "System.ComponentModel.Composition": "4.5.0", - "System.Threading.Tasks.Dataflow": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.VisualStudio.Shell.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "GaGSme4K2Wy0b65evinVk7JCl1+Dn7TCxBBwKuzATjWWiY0lvvqLElhXBq+qpC23YN0I5jTBTv0inhZ3G06uMg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "CiHRisP0ts0lbyZ1SR0H7zfiVDTgkQTeORQTDY1xOTy7WL1hIzqmUz/M5UL7d6WcVI4hTAFosJ77NcqYrnTwGg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "doH7HCWzS2bXohKR0Vl1DRjzqV0iw/poZteLS0i4nzPqFs8e2xg+U6a/ysUQ3QHgZ5y7iraefPPmp/VMJfbIeQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "SfZOz3igcZc7mKOBOr2MrPnliJL0HWF0s9UAL1B05Rc8cI/wBNIbyC1PE8Xt44P6c1vjJm6tWscAFSHZdKpGuA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.8.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "rLDc0KVPc1HZEWd5OYyvKGJ2+0eay9gNeh2wMiEJC7UNLHo+JM0wxAYNehze50hWJKlFqefHKJ6Klmp1iX3kpQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Shell.Interop.9.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "bz8tlDefGkBIGfF5KSScfs6f7P79IYzX3mqKAlXUshZjYQNd+8hLngls19cCP4oNlOre/DXAhESptNOgVihzzg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TaskRunnerExplorer.14.0": { - "type": "Transitive", - "resolved": "14.0.0", - "contentHash": "iZpAv8bEWjkyxFF1GIcSOfldqP/umopJKnJGKHa0vg8KR7ZY3u3dWtJmwO4w3abIx+176SIkQe78y5A+/Md7FA==" - }, - "Microsoft.VisualStudio.Telemetry": { - "type": "Transitive", - "resolved": "16.3.250", - "contentHash": "Ijo4HUinCwSdgegeXXzfEYmWuVLC1o9CI3FXW2x4CPKoYemlrN6xcGDUy4oKRxyOsFkjAaiRxR/X9TOgy2xVsQ==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Microsoft.VisualStudio.RemoteControl": "16.3.41", - "Microsoft.VisualStudio.Utilities.Internal": "16.3.23", - "Newtonsoft.Json": "9.0.1" - } - }, - "Microsoft.VisualStudio.Text.Data": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "lrqBhf4lI8Xzk0Z5hwcAtqnYTL1OK+iwu6tHJXDR8Vv7hFvG/YwetOQMJPz6UaLd93PEX+jPh0IfdY/CddoVDQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Threading": "17.0.63" - } - }, - "Microsoft.VisualStudio.Text.Logic": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "ZLMg3fDYijfur0B02/tkB1lmUWQtRfC9xkRr2XlQ/x7MOotTvDQO3HJp9hZyqn9LVBTWbjOnLVcoSmzthsGnnw==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "System.Collections.Immutable": "5.0.0", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Text.UI": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "f+7L6k+eyJtQzrr//j2ZtYRDnGJeGSluT10do9/3ajgfT3WpRdlq5HEr60imnHbUU5eY2V/bjbKIu8q6Yg2VfA==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "System.ComponentModel.Composition": "4.5.0" - } - }, - "Microsoft.VisualStudio.Text.UI.Wpf": { - "type": "Transitive", - "resolved": "17.0.487", - "contentHash": "Aq+eNxbpBlU8RYsSwh2JM1ZsttVvU7BKgFG6I/356EaY1ZJVz2LhAfjAl6C/A3pmizxgcczQZOIIJsrhcP/OIQ==", - "dependencies": { - "Microsoft.VisualStudio.CoreUtility": "17.0.487", - "Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime": "17.0.31723.112", - "Microsoft.VisualStudio.Text.Data": "17.0.487", - "Microsoft.VisualStudio.Text.Logic": "17.0.487", - "Microsoft.VisualStudio.Text.UI": "17.0.487" - } - }, - "Microsoft.VisualStudio.TextManager.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "w4+TDaisrknpeXkeebsTQsgl+6gbJivFyZp18/7Yqpo/lyG+IttvE9ZnJ5fFofTyrmgSGLi9BLEifT0mtsEaXA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ODXNlirs3ttpe1ooEDsKKw6XbQ06h3kvJua++J0gPqM6vMWS5YfJVH0UxHQ+BDAFZQnUJcbH9b0sVdC9JUKQ6g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "7NvcHy27r3fmlgdxPrZy4LWAm3yFqNJB3vMTMJv0K/olUzIpNRXYJMeQNltb8AJuOrbpb6HV6L2MIyJDehEADg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.12.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "2ptvBpyBVEtxwUS+FfFKPjbj+l6u/3IyznQegMmEIoQda6oeg07TFL0VAVmsqg48s4FFtpqRklx8kWhMNZqSXQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.8.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "70mNZyDPIsC+rNov2wHYb6H+V8Woc/FJktj2JsA194eMxk+DPpF0Jx3/sMgkDycV+CjIJog6TguywF4q4milIA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextManager.Interop.9.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "s59cD2jJpwEGs6HE0fbp0sE+ZsR4pp8bK91WCelRUVCiMNMtgzb3PVBCjvl082f9UxIsClOBBYDTUjrt/CFqVQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "IYWBsY2RWM8p2blyT0ja94+YRSc0wmL5er4/DfQShc3JkrQpVmmnWwSjRWC5IEp/dSukMW1LEfStqi/Kc6ZTtQ==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "rCQLkYDAKJ4BGot0CL5dkYhwArR3x2COIiTF5z3cMONVVfQqoXJKXaXve/O22DaTE/28Zt5EnaDNzZHIwROpeg==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces.11.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces.10.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KsY6a4Nk+wOj/0aGAE21JSglQt6+tlsAyPnVyiqqHn+ir1Wx9o0rFtekc6EfSle8nn5RM7QqLGSUR/Bxy6rQbA==" - }, - "Microsoft.VisualStudio.TextTemplating.Interfaces.11.0": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "pDYRlZTH42mS+r+dIjce6KOt/3HSxX1M1tPldT8VySnQ2PsT/6kRK2UVMOPla9hfJ1dWj2wMpYF8UNN2nuzdjA==", - "dependencies": { - "Microsoft.VisualStudio.TextTemplating.Interfaces.10.0": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.TextTemplating.VSHost": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "srtrkd9VCmGFRvMNV6STSe0XqM/DSl7xfY+EUIWhPVO8k+dl+DBNPIZzCK43Enfb+i3OrYFY2yqB6baNaelnaA==", - "dependencies": { - "Microsoft.VisualStudio.Shell.Framework": "17.0.31902.203", - "Microsoft.VisualStudio.TextTemplating": "17.0.31902.203", - "Microsoft.VisualStudio.Validation": "17.0.28", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "Microsoft.VisualStudio.Threading": { - "type": "Transitive", - "resolved": "17.0.64", - "contentHash": "HD/yoC7u1Ignj/EsCST4iFXl8zaE+8r2A+4CUkl6GLTJjdNjfl8iNvhqpyK8+DjCMwhyNRRH0I6S6FA37fz95Q==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.VisualStudio.Threading.Analyzers": "17.0.64", - "Microsoft.VisualStudio.Validation": "16.10.35", - "Microsoft.Win32.Registry": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Transitive", - "resolved": "17.0.64", - "contentHash": "+xz3lAqA3h2/5q6H7Udmz9TsxDQ99O+PjoQ4k4BTO3SfAfyJX7ejh7I1D1N/M/GzGUci9YOUpr6KBO4vXLg+zQ==" - }, - "Microsoft.VisualStudio.Utilities": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "j0c5dq1ZmFFAi4kB4wJ1BOUrb1kJyhFvaqDKymbdJNaib8DdG8PNAdLrW1l0RewsEMDy5Rh2cO7mzrG/FlyE5Q==", - "dependencies": { - "Microsoft.ServiceHub.Client": "3.0.3078", - "Microsoft.VisualStudio.RpcContracts": "17.0.51", - "Microsoft.VisualStudio.Telemetry": "16.3.250", - "System.ComponentModel.Composition": "4.5.0", - "System.Threading.AccessControl": "5.0.0", - "System.Threading.Tasks.Dataflow": "5.0.0" - } - }, - "Microsoft.VisualStudio.Utilities.Internal": { - "type": "Transitive", - "resolved": "16.3.23", - "contentHash": "AxbS8vXJj0IjTv67JbmOqwJERYUDE7BHbXYkXGiyqYblizMjhVdohNIethnJX9lVN2RmotN5GQbwLWDoMKatvw==" - }, - "Microsoft.VisualStudio.Validation": { - "type": "Transitive", - "resolved": "17.0.28", - "contentHash": "qT+0Qv7lxLt7NKQjkroi34s8cDXVPWA3vDkvoFZwM9PRmZ28aKrMLaQRnkT7rgBYLf+mNtr2najktKUzkAtP6Q==" - }, - "Microsoft.VisualStudio.VCProjectEngine": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "QjjZguY7FypxVasdPihUbVSP5PjwBPiitCgnC/ZPePFO5KYzJ99VxdlASDDxNGVq5+q/+3YlBD94PXComDRu2w==" - }, - "Microsoft.VisualStudio.VSHelp": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "PT4aoOrZfPHIpTq3lwamypIrx0hR7d5iLlHewwhjUNezQ8ElN/jbWjvx74r/uf2g6dVPiQcS4HI0RGgAb4MQ2A==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.VSHelp80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "B/G69KkAcMIwXgEvGAMpGSq1PvMx9JKAVzQdKiX4gm2DMFEsT2Id89TxS0oPg2X7QC3JBCk+/+BDUjtWPkjqMA==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.WCFReference.Interop": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "45HixSYn5IBSgc6MmVHhivWCiRgdZ8BruXOC9fUrQIRrYHu8DR9aw8imnFImmJY3Mii92VHV4NKPYOgUkXNd/Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "Microsoft.VisualStudio.Web.BrowserLink.12.0": { - "type": "Transitive", - "resolved": "12.0.0", - "contentHash": "HeuaZh8+wNVdwx7VF8guFGH2Z2zH+FYxWBsRNp+FjjlmrhCfM7GUQV5azaTv/bN5TPaK8ALJoP9UX5o1FB5k1A==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==" - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Nerdbank.Streams": { - "type": "Transitive", - "resolved": "2.6.81", - "contentHash": "htBHFE359qyyFwrvAGvFxrbBAoldZdl0XjtQdDWTJ8t5sWWs7QVXID5y1ZGJE61UgpV5CqWsj/NT0LOAn5GdZw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.VisualStudio.Threading": "16.7.56", - "Microsoft.VisualStudio.Validation": "15.5.31", - "System.IO.Pipelines": "4.7.2", - "System.Net.WebSockets": "4.3.0", - "System.Runtime.CompilerServices.Unsafe": "4.7.1" - } - }, - "stdole": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "c1WK5n8poXt1fafyuS+ntL5w0tc3P/r4F7+aeUaLYTp2/YoQB8XUpQFWqytZdtu7EAuMXoZ14T7jh7Vl+M4ZfQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "StreamJsonRpc": { - "type": "Transitive", - "resolved": "2.8.28", - "contentHash": "i2hKUXJSLEoWpPqQNyISqLDqmFHMiyasjTC/PrrHNWhQyauFeVoebSct3E4OTUzRC1DYjVJ9AMiVbp/uVYLnjQ==", - "dependencies": { - "MessagePack": "2.2.85", - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.VisualStudio.Threading": "16.9.60", - "Nerdbank.Streams": "2.6.81", - "Newtonsoft.Json": "12.0.2", - "System.Collections.Immutable": "5.0.0", - "System.Diagnostics.DiagnosticSource": "5.0.1", - "System.IO.Pipelines": "5.0.1", - "System.Memory": "4.5.4", - "System.Net.Http": "4.3.4", - "System.Net.WebSockets": "4.3.0", - "System.Reflection.Emit": "4.7.0", - "System.Threading.Tasks.Dataflow": "5.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "StrongNamer": { - "type": "Transitive", - "resolved": "0.0.8", - "contentHash": "7GAMIhQSKabPrXVtEpDQoLRWVZUn1IGzeHvfDSvs5lPhU4KPFMQVpI/lDFikhMXr8FmdFBOBGaHEhQ9azMK3TA==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", - "dependencies": { - "System.Memory": "4.5.4" - } - }, - "System.ComponentModel.Composition": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+iB9FoZnfdqMEGq6np28X6YNSUrse16CakmIhV3h6PxEWt7jYxUN3Txs1D8MZhhf4QmyvK0F/EcIN0f4gGN0dA==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", - "dependencies": { - "System.Composition.AttributedModel": "1.0.31", - "System.Composition.Convention": "1.0.31", - "System.Composition.Hosting": "1.0.31", - "System.Composition.Runtime": "1.0.31", - "System.Composition.TypedParts": "1.0.31" - } - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", - "dependencies": { - "System.Composition.AttributedModel": "1.0.31" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", - "dependencies": { - "System.Composition.Runtime": "1.0.31" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "1.0.31", - "contentHash": "0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", - "dependencies": { - "System.Composition.AttributedModel": "1.0.31", - "System.Composition.Hosting": "1.0.31", - "System.Composition.Runtime": "1.0.31" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "uXQEYqav2V3zP6OwkOKtLv+qIi6z3m1hsGyKwXX7ZA7htT4shoVccGxnJ9kVRFPNAsi1ArZTq2oh7WOto6GbkQ==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "5.0.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.4", - "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", - "dependencies": { - "System.Security.Cryptography.X509Certificates": "4.3.0" - } - }, - "System.Net.WebSockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "u6fFNY5q4T8KerUAVbya7bR6b7muBuSTAersyrihkcmE5QhEOiH3t5rh4il15SexbVlpXFHGuMwr/m8fDrnkQg==" - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Private.Uri": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "o1+7RJnu3Ik3PazR7Z7tJhjPdE000Eq2KGLLWhqJJKXj04wrS8lwb1OFtDF9jzXXADhUuZNJZlPc98uwwqmpFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "j/V5HVvxvBQ7uubYD0PptQW2KGsi1Pc2kZ9yfwLixv3ADdjL/4M78KyC5e+ymW612DY8ZE4PFoZmWpoNmN2mqg==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", - "dependencies": { - "System.Collections.Immutable": "5.0.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==" - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.2" - } - }, - "System.Threading.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "WJ9w9m4iHJVq0VoH7hZvYAccbRq95itYRhAAXd6M4kVCzLmT6NqTwmSXKwp3oQilWHhYTXgqaIXxBfg8YaqtmA==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NBp0zSAMZp4muDje6XmbDfmkqw9+qsDCHp+YMEtnVgHEjQZ3Q7MzFTTp3eHqpExn4BwMrS7JkUVOTcVchig4Sw==" - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "VSLangProj": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "XjyhxIyXjPitKrxeWkF1yaiqT02Klct5yZHz93x9O4nLTEnbmAHb+HRNiIIgNMXvG2sboNIvTt1MpuR1AeJktw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj100": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "8pwHJP2XefsWxhL3dQ11MBD9WwxP3SjAO41t6ZcFSfTHtSQFsp7erdtBrKzuFXpnIp3WoVc26f7jWrt4rr66+g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj110": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "UToccqkFdocw9LZx7Rn62/puqxPjF7MQWIk2yMGqIdHLcaeRmR6gI1/3Ea+57ZcVehhuEvtxH8jXV2t4HwwU8g==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj140": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "ssRCHLYW5nSJJBy3rad5JId8SjcXHOsOzVW4ohZgtnHBtf73ehIJLtr+oRc7JK3NasmWBuB9DJqwwJBLj4wn3Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj150": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "sxFOk5x5bDqyxUgfi5zF58E8BnfRQ17Ft0IbxJDS0Bvf+6CLYjWQzKXqxCczYHs2aD7upUpN1hrf93bHlqahAQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj157": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "jYp9X00aaJKttYL1IuME8mpvd349H2zDbaFIBRf6LnDUPHd7i7MsnaREpID1Dr+H37P16pzbv48taLKUnAoBsw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj158": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "bwHHmgclOjBNI/CpUqgPH274D7UKDrgzFAEtFULixlOhA6E4NfYOgdUdemiW5bg58i4ao60T9tz3ac6PQfHeAw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj165": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "Brh37fZOyPL0zC9XVtOzWRl22GZZnN6/TVEvg6PDhWhjgnkdHv8hz0GZCIzilbyl9ilOJnQCx8oVQ+KMfcvXFQ==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj2": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "hqP1IODtJneTrUwwXeqD0YHmqyde3/KJaJRJehsn07TQxYxzyigrpNsh8GEoExTSis0SoFt97tXTztbXiBvSvg==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj80": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "qm0T7dPige52L6mnsk+3Q8QV8R/Rgdkdpb3FaUMolP4JWU4s2OfrUzFajFBf8auf/NBQgUcBvE1Kr5yRpYmL2Q==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "VSLangProj90": { - "type": "Transitive", - "resolved": "17.0.31902.203", - "contentHash": "KoyI+jvJ9fHwR2uGGlUxOqAmTgDO1naxwXbt504y/Jx1h/BXnhCWipGdyM/lS83GUGsmx8RhZbiiMlZw7LmYhw==", - "dependencies": { - "Microsoft.VisualStudio.Interop": "17.0.31902.203" - } - }, - "SonarLint.VisualStudio.ConnectedMode": { - "type": "Project", - "dependencies": { - "Microsoft.Alm.Authentication": "[4.0.0.1, )", - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", - "SonarLint.VisualStudio.SLCore": "[1.0.0, )", - "StrongNamer": "[0.0.8, )" - } - }, - "SonarLint.VisualStudio.Core": { - "type": "Project", - "dependencies": { - "BouncyCastle.Cryptography": "[2.4.0, )", - "Newtonsoft.Json": "[13.0.3, )", - "System.Collections.Immutable": "[5.0.0, )", - "System.IO.Abstractions": "[9.0.4, )", - "System.Threading.Channels": "[7.0.0, )" - } - }, - "SonarLint.VisualStudio.Infrastructure.VS": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )" - } - }, - "SonarLint.VisualStudio.IssueVisualization": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarLint.VisualStudio.SLCore": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )" - } - }, - "SonarLint.VisualStudio.SLCore": { - "type": "Project", - "dependencies": { - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "StreamJsonRpc": "[2.6.121, )" - } - }, - "sonarqube.client": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "System.Net.Http": "[4.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/SLCore.IntegrationTests/packages.lock.json b/src/SLCore.IntegrationTests/packages.lock.json index 5860db2956..cd6914598b 100644 --- a/src/SLCore.IntegrationTests/packages.lock.json +++ b/src/SLCore.IntegrationTests/packages.lock.json @@ -1260,7 +1260,6 @@ "SonarLint.VisualStudio.Integration": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )", "SonarLint.VisualStudio.IssueVisualization.Security": "[1.0.0, )", - "SonarLint.VisualStudio.Roslyn.Suppressions": "[1.0.0, )", "SonarLint.VisualStudio.RoslynAnalyzerServer": "[1.0.0, )", "SonarLint.VisualStudio.SLCore": "[1.0.0, )", "SonarLint.VisualStudio.SLCore.Listeners": "[1.0.0, )", @@ -1377,18 +1376,6 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" - } - }, "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { diff --git a/src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs b/src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs deleted file mode 100644 index b8cf60fcef..0000000000 --- a/src/SonarQube.Client.Tests/Helpers/ComponentKeyGeneratorTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Helpers; - -namespace SonarQube.Client.Tests.Helpers; - -[TestClass] -public class ComponentKeyGeneratorTests -{ - [DataTestMethod] - [DataRow(@"c:\dir\dir\root\dir\file.cs", @"c:\dir\dir\root\", "ProjectKey", "ProjectKey:dir/file.cs")] - [DataRow(@"c:\dir\dir\root\file.cs", @"c:\dir\dir\root\", "ProjectKey", "ProjectKey:file.cs")] - [DataRow(@"c:\dir\dir\root\dir1\dir2\file.cs", @"c:\dir\dir\root\", "Project:Key", "Project:Key:dir1/dir2/file.cs")] - public void GetComponentKey(string filePath, string rootPath, string projectName, string expected) - { - ComponentKeyGenerator.GetComponentKey(filePath, rootPath, projectName).Should().Be(expected); - } - - [TestMethod] - public void GetComponentKey_NonRootedPath_Throws() - { - Action act = () => { ComponentKeyGenerator.GetComponentKey(@"c:\dir\dir\root\dir\file.cs", @".\non\rooted\", "project"); }; - - act.Should().ThrowExactly().WithMessage("Invalid root path format"); - } - - [TestMethod] - public void GetComponentKey_NonFolderPath_Throws() - { - Action act = () => { ComponentKeyGenerator.GetComponentKey(@"c:\dir\dir\root\dir\file.cs", @"c:\not\folder", "project"); }; - - act.Should().ThrowExactly().WithMessage("Invalid root path format"); - } - - [TestMethod] - public void GetComponentKey_NonMatchingRoot_Throws() - { - Action act = () => { ComponentKeyGenerator.GetComponentKey(@"c:\dir\dir\root\dir\file.cs", @"c:\not\same\folder\", "project"); }; - - act.Should().ThrowExactly().WithMessage("Local path is not under this root"); - } -} diff --git a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs b/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs deleted file mode 100644 index d0be76ba7c..0000000000 --- a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueSeverityConverterTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Helpers -{ - [TestClass] - public class SonarQubeIssueSeverityConverterTests - { - [TestMethod] - // Exact matches - [DataRow("BLOCKER", SonarQubeIssueSeverity.Blocker)] - [DataRow("CRITICAL", SonarQubeIssueSeverity.Critical)] - [DataRow("INFO", SonarQubeIssueSeverity.Info)] - [DataRow("MAJOR ", SonarQubeIssueSeverity.Major)] - [DataRow("MINOR", SonarQubeIssueSeverity.Minor)] - - // Case-insensitivity - [DataRow("blocker", SonarQubeIssueSeverity.Blocker)] - [DataRow("minOR", SonarQubeIssueSeverity.Minor)] - - // Unknowns - [DataRow(null, SonarQubeIssueSeverity.Unknown)] - [DataRow("", SonarQubeIssueSeverity.Unknown)] - [DataRow("MAJORXXX", SonarQubeIssueSeverity.Unknown)] - [DataRow("foo", SonarQubeIssueSeverity.Unknown)] - public void SeverityConversion(string inputData, SonarQubeIssueSeverity expectedResult) - { - SonarQubeIssueSeverityConverter.Convert(inputData).Should().Be(expectedResult); - } - } -} diff --git a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs b/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs deleted file mode 100644 index 0f35d37a34..0000000000 --- a/src/SonarQube.Client.Tests/Helpers/SonarQubeIssueTypeConverterTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Helpers -{ - [TestClass] - public class SonarQubeIssueTypeConverterTests - { - [TestMethod] - // Exact matches - [DataRow("CODE_SMELL", SonarQubeIssueType.CodeSmell)] - [DataRow("BUG", SonarQubeIssueType.Bug)] - [DataRow("VULNERABILITY", SonarQubeIssueType.Vulnerability)] - [DataRow("SECURITY_HOTSPOT", SonarQubeIssueType.SecurityHotspot)] - - // Case-insensitivity - [DataRow("bug", SonarQubeIssueType.Bug)] - [DataRow("security_hotspot", SonarQubeIssueType.SecurityHotspot)] - - // Unknowns - [DataRow(null, SonarQubeIssueType.Unknown)] - [DataRow("", SonarQubeIssueSeverity.Unknown)] - [DataRow("BUGX", SonarQubeIssueSeverity.Unknown)] - [DataRow("foo", SonarQubeIssueSeverity.Unknown)] - public void Convert(string inputData, SonarQubeIssueType expectedResult) - { - SonarQubeIssueTypeConverter.Convert(inputData).Should().Be(expectedResult); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/IssueFlowTests.cs b/src/SonarQube.Client.Tests/Models/IssueFlowTests.cs deleted file mode 100644 index 56b7cb6b6c..0000000000 --- a/src/SonarQube.Client.Tests/Models/IssueFlowTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Models -{ - [TestClass] - public class IssueFlowTests - { - [TestMethod] - public void Ctor_LocationsAreNeverNull() - { - var testSubject = new IssueFlow(null); - - testSubject.Locations.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_PropertiesAreSet() - { - var locations = new List - { - new IssueLocation("file1", "component1", null, "message1"), - new IssueLocation("file2", "component2", null, "message2") - }; - - var testSubject = new IssueFlow(locations); - - testSubject.Locations.Should().BeEquivalentTo(locations[0], locations[1]); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs b/src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs deleted file mode 100644 index a678e75b3c..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerExclusionsTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Models -{ - [TestClass] - public class ServerExclusionsTests - { - [TestMethod] - public void Equals_OtherObjIsNull_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_ExclusionsAreDifferent_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion2" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(other).Should().BeFalse(); - } - - [TestMethod] - public void Equals_GlobalExclusionsAreDifferent_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global2" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(other).Should().BeFalse(); - } - - [TestMethod] - public void Equals_InclusionsAreDifferent_False() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion2" }); - - testSubject.Equals(other).Should().BeFalse(); - } - - [TestMethod] - public void Equals_EverythingIsTheSame_True() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - var other = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global1" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(other).Should().BeTrue(); - } - - [TestMethod] - public void Equals_SameReference_True() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "exclusion1" }, - globalExclusions: new[] { "global" }, - inclusions: new[] { "inclusion1" }); - - testSubject.Equals(testSubject).Should().BeTrue(); - } - - [TestMethod] - public void Ctor_HasExclusions_AppendPathPrefix() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "path1", "**\\path2", "**/path3", "**path4", "*/*" }, - globalExclusions: Array.Empty(), - inclusions: null); - - testSubject.Exclusions.Should().BeEquivalentTo( - "**/path1", - "**/**\\path2", - "**/path3", - "**/**path4", - "**/*/*"); - testSubject.GlobalExclusions.Should().BeEmpty(); - testSubject.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_HasGlobalExclusions_AppendPathPrefix() - { - var testSubject = new ServerExclusions( - exclusions: null, - globalExclusions: new[] { "path1", "**\\path2", "**/path3", "**path4", "*/*" }, - inclusions: Array.Empty()); - - testSubject.Exclusions.Should().BeEmpty(); - testSubject.GlobalExclusions.Should().BeEquivalentTo( - "**/path1", - "**/**\\path2", - "**/path3", - "**/**path4", - "**/*/*"); - testSubject.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_HasInclusions_AppendPathPrefix() - { - var testSubject = new ServerExclusions( - exclusions: Array.Empty(), - globalExclusions: null, - inclusions: new[] {"path1", "**\\path2", "**/path3", "**path4", "*/*"}); - - testSubject.Exclusions.Should().BeEmpty(); - testSubject.GlobalExclusions.Should().BeEmpty(); - testSubject.Inclusions.Should().BeEquivalentTo( - "**/path1", - "**/**\\path2", - "**/path3", - "**/**path4", - "**/*/*"); - } - - [TestMethod] - public void ToDictionary_HasExclusions_ReturnsConcatenatedValues() - { - var testSubject = new ServerExclusions( - exclusions: new[] { "**/path1", "**/*/path2" }, - globalExclusions: new[] { "**/path1" }, - inclusions: null); - - var result = testSubject.ToDictionary(); - - result.Should().BeEquivalentTo( - new Dictionary - { - {"sonar.exclusions", "**/path1,**/*/path2"}, - {"sonar.global.exclusions", "**/path1"}, - {"sonar.inclusions", ""} - }); - } - - [TestMethod] - public void ToDictionary_HasGlobalExclusions_ReturnsConcatenatedValues() - { - var testSubject = new ServerExclusions( - exclusions: null, - globalExclusions: new[] { "**/path1", "**/*/path2" }, - inclusions: new[] { "**/path1" }); - - var result = testSubject.ToDictionary(); - - result.Should().BeEquivalentTo( - new Dictionary - { - {"sonar.exclusions", ""}, - {"sonar.global.exclusions", "**/path1,**/*/path2"}, - {"sonar.inclusions", "**/path1"} - }); - } - - [TestMethod] - public void ToDictionary_HasInclusions_ReturnsConcatenatedValues() - { - var testSubject = new ServerExclusions( - exclusions: new[] {"**/path1"}, - globalExclusions: null, - inclusions: new[] {"**/path1", "**/*/path2"}); - - var result = testSubject.ToDictionary(); - - result.Should().BeEquivalentTo( - new Dictionary - { - {"sonar.exclusions", "**/path1"}, - {"sonar.global.exclusions", ""}, - {"sonar.inclusions", "**/path1,**/*/path2"} - }); - } - - - - - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs deleted file mode 100644 index 2b0e352913..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/BranchAndIssueKeyTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class BranchAndIssueKeyTests - { - [TestMethod] - public void Ctor_InvalidKey_Throws() - { - Action act = () => { new BranchAndIssueKey(null, "main"); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("issueKey"); - } - - [TestMethod] - public void Ctor_InvalidBranch_Throws() - { - Action act = () => { new BranchAndIssueKey("id1", null); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("branchName"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs deleted file mode 100644 index 8529cf72f1..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/ClientContract/IssueChangedServerEventTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents.ClientContract -{ - [TestClass] - public class IssueChangedServerEventTests - { - [TestMethod] - public void Ctor_NullIssuesArray_Throws() - { - // The implementation of ToString assumes the issues array is not null, - // so we'll check that the constructor actually enforces this - Action act = () => new IssueChangedServerEvent("any", true, null); - act.Should().Throw().And.ParamName.Should().Be("issues"); - } - - [TestMethod] - public void ToString_EmptyList_ContainsExpectedStrings() - { - var testSubject = new IssueChangedServerEvent("projectKey A", true, Array.Empty()); - - var actual = testSubject.ToString(); - - actual.Should().ContainAll("projectKey A", "True", "0"); - } - - [TestMethod] - public void ToString_MultipleItemsInList_ContainsExpectedStrings() - { - var testSubject = new IssueChangedServerEvent("projectKey B", false, - new BranchAndIssueKey[] - { - new BranchAndIssueKey("issuekey1", "branch1"), - new BranchAndIssueKey("issuekey2", "branch2") - }); - - var actual = testSubject.ToString(); - - actual.Should().ContainAll("projectKey B", "False", "2", - "issuekey1", "branch1", - "issuekey2", "branch2"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs deleted file mode 100644 index 917c1897da..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/IssueChangedServerEventTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class IssueChangedServerEventTests - { - [TestMethod] - public void Ctor_InvalidIssuesList_Throws() - { - Action act = () => { new IssueChangedServerEvent("MyProject", false, null); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("issues"); - } - - [TestMethod] - public void Ctor_InvalidProjectKey_Throws() - { - Action act = () => { new IssueChangedServerEvent(null, false, new[]{new BranchAndIssueKey("i", "b")}); }; - - act.Should().ThrowExactly().And.ParamName.Should().Be("projectKey"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs deleted file mode 100644 index cf7a2bcf76..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderFactoryTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Threading; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Logging; -using SonarQube.Client.Models.ServerSentEvents; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SSEStreamReaderFactoryTests - { - [TestMethod] - public void Create_CreatesSSEStreamReader() - { - var testSubject = new SSEStreamReaderFactory(Mock.Of()); - - var result = testSubject.Create(Stream.Null, CancellationToken.None); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs deleted file mode 100644 index cf1b89a651..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SSEStreamReaderTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Moq; -using Newtonsoft.Json; -using SonarQube.Client.Logging; -using SonarQube.Client.Models.ServerSentEvents; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; -using SonarQube.Client.Tests.Infra; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SSEStreamReaderTests - { - [TestMethod] - public void ReadAsync_UnderlyingReaderException_LogsDisposesAndThrows() - { - var streamReader = new Mock(); - var exceptionMessage = "Some network error"; - streamReader.Setup(reader => reader.ReadAsync()).Throws(new Exception(exceptionMessage)); - - var testSubject = CreateTestSubject(streamReader.Object); - - Func act = () => testSubject.ReadAsync(); - - act.Should().Throw().Which.Message.Should().Be(exceptionMessage); - streamReader.Verify(reader => reader.Dispose(), Times.Once); - } - - [TestMethod] - public async Task ReadAsync_Null_NullReturned() - { - var sqSSEStreamReader = CreateSqStreamReader((ISqServerEvent)null); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - } - - [TestMethod] - [Description("SQ stream events that we do not support yet. We need to ignore them.")] - public async Task ReadAsync_UnrecognizedEventType_NullReturned() - { - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("some type 111", "some data")); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task ReadAsync_FailureToDeserializeTheEventData_ExceptionLoggedAndNullReturned() - { - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("IssueChanged", "some invalid data")); - var logger = new TestLogger(); - - var testSubject = CreateTestSubject(sqSSEStreamReader, logger); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - - logger.DebugMessages.Should().Contain(x => - x.Contains(nameof(JsonReaderException)) && - x.Contains("IssueChanged") && - x.Contains("some invalid data")); - } - - [TestMethod, Description("Missing mandatory 'branchName' field")] - public async Task ReadAsync_IssueChangedEventType_MissingMandatoryFields_ExceptionLoggedAndNullReturned() - { - const string serializedIssueChangedEvent = - "{\"projectKey\": \"projectKey1\",\"issues\": [{\"issueKey\": \"key1\"}],\"resolved\": \"true\"}"; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("IssueChanged", serializedIssueChangedEvent)); - var logger = new TestLogger(); - - var testSubject = CreateTestSubject(sqSSEStreamReader, logger); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - - logger.DebugMessages.Should().Contain(x => - x.Contains(nameof(ArgumentNullException)) && - x.Contains("branchName") && - x.Contains("IssueChanged") && - x.Contains("projectKey1")); - } - - [TestMethod] - public async Task ReadAsync_IssueChangedEventType_DeserializedEvent() - { - const string serializedIssueChangedEvent = - "{\"projectKey\": \"projectKey1\",\"issues\": [{\"issueKey\": \"key1\",\"branchName\": \"master\"}],\"resolved\": \"true\"}"; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("IssueChanged", serializedIssueChangedEvent)); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.Should().BeEquivalentTo( - new IssueChangedServerEvent( - projectKey: "projectKey1", - isResolved: true, - issues: new[] { new BranchAndIssueKey("key1", "master") })); - } - - [TestMethod] - public async Task ReadAsync_QualityProfileEventType_DeserializedEvent() - { - const string serializedQualityProfileEvent = """ - { - "projects": [ - "ABC" - ], - "activatedRules": [ - { - "key": "javascript:S4139", - "language": "js", - "severity": "MAJOR", - "params": [] - } - ], - "deactivatedRules": [] - } - """; - - var sqSSEStreamReader = CreateSqStreamReader(new SqServerEvent("RuleSetChanged", serializedQualityProfileEvent)); - - var testSubject = CreateTestSubject(sqSSEStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - // note: event implementation is empty, no test for data validity here either - } - - private ISqSSEStreamReader CreateSqStreamReader(params ISqServerEvent[] events) - { - var streamReader = new Mock(); - var sequenceSetup = streamReader.SetupSequence(x => x.ReadAsync()); - - foreach (var sqServerEvent in events) - { - sequenceSetup.ReturnsAsync(sqServerEvent); - } - - return streamReader.Object; - } - - private SSEStreamReader CreateTestSubject(ISqSSEStreamReader sqSSEStreamReader, ILogger logger = null) - { - logger ??= Mock.Of(); - - return new SSEStreamReader(sqSSEStreamReader, logger); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs deleted file mode 100644 index 590e931963..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqSSEStreamReaderTests.cs +++ /dev/null @@ -1,171 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SqSSEStreamReaderTests - { - [TestMethod, Timeout(10000)] - [DataRow("")] - [DataRow("some data\nanother data")] - public async Task ReadAsync_EndOfStream_TaskFinishes(string content) - { - var networkStreamReader = CreateNetworkStreamReader(content); - - var testSubject = CreateTestSubject(networkStreamReader); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - networkStreamReader.EndOfStream.Should().BeTrue(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_TokenIsCancelledBeforeStreamIsFinished_TaskFinishes() - { - var networkStreamReader = CreateNetworkStreamReader(content: "some data\nanother data\n"); - var cancellationToken = new CancellationTokenSource(); - cancellationToken.Cancel(); - - var testSubject = CreateTestSubject(networkStreamReader, token: cancellationToken.Token); - - var result = await testSubject.ReadAsync(); - - result.Should().BeNull(); - networkStreamReader.EndOfStream.Should().BeFalse(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_StreamLinesAreAggregatedUntilAnEmptyLine() - { - var networkStreamReader = CreateNetworkStreamReader(content: "line 1\nline 2\nline 3\n\nline 4\nline 5\n\nline 6\nline 7\n"); - var parsedEvent1 = Mock.Of(); - var parsedEvent2 = Mock.Of(); - - var sqServerSentEventParser = new Mock(); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] {"line 1", "line 2", "line 3"})) - .Returns(parsedEvent1); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] { "line 4", "line 5" })) - .Returns(parsedEvent2); - - var testSubject = CreateTestSubject(networkStreamReader, sqServerSentEventParser.Object); - - var actualEvent1 = await testSubject.ReadAsync(); - actualEvent1.Should().Be(parsedEvent1); - networkStreamReader.EndOfStream.Should().BeFalse(); - - var actualEvent2 = await testSubject.ReadAsync(); - actualEvent2.Should().Be(parsedEvent2); - networkStreamReader.EndOfStream.Should().BeFalse(); - - var actualEvent3 = await testSubject.ReadAsync(); - actualEvent3.Should().BeNull(); - networkStreamReader.EndOfStream.Should().BeTrue(); - - sqServerSentEventParser.VerifyAll(); - sqServerSentEventParser.VerifyNoOtherCalls(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_FailureToParseAnEvent_EventIsIgnored() - { - var networkStreamReader = CreateNetworkStreamReader(content: "line 1\n\nline 2\nline 3\n\n"); - var sqServerSentEventParser = new Mock(); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] { "line 1" })) - .Returns((ISqServerEvent) null); - - var parsedEvent = Mock.Of(); - - sqServerSentEventParser - .Setup(x => x.Parse(new[] { "line 2", "line 3" })) - .Returns(parsedEvent); - - var testSubject = CreateTestSubject(networkStreamReader, sqServerSentEventParser.Object); - - var actualEvent = await testSubject.ReadAsync(); - actualEvent.Should().Be(parsedEvent); - networkStreamReader.EndOfStream.Should().BeTrue(); - - sqServerSentEventParser.VerifyAll(); - sqServerSentEventParser.VerifyNoOtherCalls(); - } - - [TestMethod, Timeout(10000)] - public async Task ReadAsync_StreamCrashesInTheMiddle_Exception() - { - var networkStreamReader = CreateNetworkStreamReader(content: "line 1\n\nline 2\nline 3\n\n"); - var cancellationToken = new CancellationTokenSource(); - cancellationToken.Cancel(); - - var testSubject = CreateTestSubject(networkStreamReader, token: cancellationToken.Token); - - await testSubject.ReadAsync(); - networkStreamReader.Close(); - - Func> func = async () => await testSubject.ReadAsync(); - - func.Should().ThrowExactly().And.Message.Should().Be("Cannot read from a closed TextReader."); - } - - [TestMethod] - public void Dispose_ClosesStream() - { - var networkStreamReader = CreateNetworkStreamReader(content: "content"); - - var testSubject = CreateTestSubject(networkStreamReader); - - networkStreamReader.BaseStream.Should().NotBeNull(); - - testSubject.Dispose(); - - networkStreamReader.BaseStream.Should().BeNull(); - } - - private static StreamReader CreateNetworkStreamReader(string content) => - new(new MemoryStream(Encoding.UTF8.GetBytes(content))); - - private static SqSSEStreamReader CreateTestSubject( - StreamReader networkStreamReader, - ISqServerSentEventParser sqServerSentEventParser = null, - CancellationToken? token = null) - { - token ??= CancellationToken.None; - - return new SqSSEStreamReader(networkStreamReader, token.Value, sqServerSentEventParser); - } - } -} diff --git a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs b/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs deleted file mode 100644 index 662372b34d..0000000000 --- a/src/SonarQube.Client.Tests/Models/ServerSentEvents/SqServerSentEventParserTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; - -namespace SonarQube.Client.Tests.Models.ServerSentEvents -{ - [TestClass] - public class SqServerSentEventParserTests - { - [TestMethod] - public void Parse_NullEventLines_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(null); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_EmptyEventLines_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(Array.Empty()); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventType_MissingEventTypeField_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] {"data: some data"}); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventType_EventTypeIsEmpty_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event: ", "data: some data"}); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventType_EventTypeIsNotInCorrectFormat_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event : extra space", "data: some data" }); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventData_MissingEventDataField_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[]{ "event: some type" }); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventData_EventDataIsEmpty_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event: some type", "data: " }); - - result.Should().BeNull(); - } - - [TestMethod] - public void Parse_InvalidEventData_EventDataIsNotInCorrectFormat_Null() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "event: some type", "data : extra space" }); - - result.Should().BeNull(); - } - - - [TestMethod] - public void Parse_CorrectEventString_ParsedEvent() - { - var eventLines = new[] - { - "event: some event type", - "data: some event data" - }; - - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(eventLines); - - result.Should().NotBeNull(); - result.Type.Should().Be("some event type"); - result.Data.Should().Be("some event data"); - } - - [TestMethod] - public void Parse_CorrectEventString_MultilineData_ParsedEvent() - { - var eventLines = new[] - { - "event: some event type", - "data: some event data1", - "data: ", - "data: some event data2", - }; - - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(eventLines); - - result.Should().NotBeNull(); - result.Type.Should().Be("some event type"); - result.Data.Should().Be("some event data1some event data2"); - } - - [TestMethod] - public void Parse_HasJunkFields_JunkFieldsIgnored() - { - var eventLines = new[] - { - "junk1: junk field1", - "EVENT: junk event type", - "event:", - "event: some event type", - "data: ", - "data: some event data1", - "junk2: junk field2", - "DATA: junk data2", - "data: some event data2", - "junk3: junk field3" - }; - - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(eventLines); - - result.Should().NotBeNull(); - result.Type.Should().Be("some event type"); - result.Data.Should().Be("some event data1some event data2"); - } - - [TestMethod] - public void Parse_EventTypeIsNotTheFirstField_EventTypeIsStillParsedCorrectly() - { - var testSubject = CreateTestSubject(); - - var result = testSubject.Parse(new[] { "data: some data", "event: some type" }); - - result.Should().NotBeNull(); - result.Type.Should().Be("some type"); - result.Data.Should().Be("some data"); - } - - - private static SqServerSentEventParser CreateTestSubject() => new(); - } -} diff --git a/src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs b/src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs deleted file mode 100644 index bb0e499848..0000000000 --- a/src/SonarQube.Client.Tests/Models/SonarQubeIssueTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests.Models -{ - [TestClass] - public class SonarQubeIssueTests - { - private static readonly DateTimeOffset ValidTimestamp = DateTimeOffset.UtcNow; - - [TestMethod] - public void Ctor_FilePathCanBeNull() - { - var testSubject = new SonarQubeIssue("issueKey", null, "hash", "message", "module", "rule", true, - SonarQubeIssueSeverity.Info, ValidTimestamp, ValidTimestamp, textRange: null, flows: null); - - testSubject.FilePath.Should().BeNull(); - } - - [TestMethod] - public void Ctor_TextRangeCanBeNull() - { - var testSubject = new SonarQubeIssue("issueKey", "file", "hash", "message", "module", "rule", true, - SonarQubeIssueSeverity.Info, ValidTimestamp, ValidTimestamp, textRange: null, flows: null); - - testSubject.TextRange.Should().BeNull(); - } - - [TestMethod] - public void Ctor_FlowsAreNeverNull() - { - var testSubject = new SonarQubeIssue("issueKey", "file", "hash", "message", "module", "rule", true, - SonarQubeIssueSeverity.Info, ValidTimestamp, ValidTimestamp, new IssueTextRange(123, 456, 7, 8), flows: null); - - testSubject.Flows.Should().BeEmpty(); - } - - [TestMethod] - public void Ctor_PropertiesAreSet() - { - var creationTimestamp = DateTimeOffset.Parse("2001-12-13T10:11:12+0000"); - var lastUpdateTimestamp = DateTimeOffset.Parse("2020-01-02T13:14:15+0000"); - - var flows = new List - { - new IssueFlow(null), new IssueFlow(null) - }; - var testSubject = new SonarQubeIssue("issueKey", "file", "hash", "message", "module", "rule", true, SonarQubeIssueSeverity.Info, - creationTimestamp, lastUpdateTimestamp, new IssueTextRange(123, 456, 7, 8), flows, "contextKey"); - - testSubject.IssueKey.Should().Be("issueKey"); - testSubject.FilePath.Should().Be("file"); - testSubject.Hash.Should().Be("hash"); - testSubject.Message.Should().Be("message"); - testSubject.ModuleKey.Should().Be("module"); - testSubject.RuleId.Should().Be("rule"); - testSubject.IsResolved.Should().BeTrue(); - testSubject.Severity.Should().Be(SonarQubeIssueSeverity.Info); - testSubject.CreationTimestamp.Should().Be(creationTimestamp); - testSubject.LastUpdateTimestamp.Should().Be(lastUpdateTimestamp); - testSubject.TextRange.Should().BeEquivalentTo(new IssueTextRange(123, 456, 7, 8)); - testSubject.Flows.Should().BeEquivalentTo(flows); - testSubject.Context.Should().Be("contextKey"); - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs deleted file mode 100644 index 3027a49240..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetExclusionsRequestTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20 -{ - [TestClass] - public class GetExclusionsRequestTests - { - [TestMethod] - [DataRow("")] - [DataRow(@"{}")] - [DataRow(@"{""settings"": []}")] - [DataRow(@"{""settings"": [{""key"": ""some.other.setting"",""values"": [""val1""]}]}")] - public async Task InvokeAsync_AllSettingAreMissing_ReturnsEmptyConfiguration(string response) - { - const string projectKey = "myproject"; - - var testSubject = CreateTestSubject(projectKey); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - var request = $"api/settings/values?component={projectKey}&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - - SetupHttpRequest(handlerMock, request, response); - - var result = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - result.Should().NotBeNull(); - - result.Exclusions.Should().BeEmpty(); - result.GlobalExclusions.Should().BeEmpty(); - result.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - [Description("This is what SonarCloud returns when the setting is not defined")] - public async Task InvokeAsync_SomeMissingSetting_ReturnsDefinedProperties() - { - const string projectKey = "myproject"; - - var testSubject = CreateTestSubject(projectKey); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - var request = $"api/settings/values?component={projectKey}&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - var response = @"{ - ""settings"": [ - { - ""key"": ""sonar.global.exclusions"", - ""values"": [ - ""**/build-wrapper-dump.json"" - ] - } - ] -}"; - - SetupHttpRequest(handlerMock, request, response); - - var result = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - result.Should().NotBeNull(); - - result.Exclusions.Should().BeEmpty(); - result.GlobalExclusions.Should().BeEquivalentTo("**/build-wrapper-dump.json"); - result.Inclusions.Should().BeEmpty(); - } - - [TestMethod] - public async Task InvokeAsync_ExistingSetting_ReturnsDefinedProperties() - { - const string projectKey = "myproject"; - - var testSubject = CreateTestSubject(projectKey); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - var request = $"api/settings/values?component={projectKey}&keys=sonar.exclusions%2Csonar.global.exclusions%2Csonar.inclusions"; - var response = @"{ - ""settings"": [ - { - ""key"": ""sonar.exclusions"", - ""values"": [ - ""**/value1"", - ""value2"", - ""some/value/3"", - ] - }, - { - ""key"": ""sonar.global.exclusions"", - ""values"": [ - ""some/value/4"", - ] - }, - { - ""key"": ""sonar.inclusions"", - ""values"": [ - ""**/111"" - ] - } - ] -}"; - - SetupHttpRequest(handlerMock, request, response); - - var result = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - result.Should().NotBeNull(); - - result.Exclusions.Should().BeEquivalentTo("**/value1", "**/value2", "**/some/value/3"); - result.GlobalExclusions.Should().BeEquivalentTo("**/some/value/4"); - result.Inclusions.Should().BeEquivalentTo("**/111"); - } - - private static GetExclusionsRequest CreateTestSubject(string projectKey) - { - var testSubject = new GetExclusionsRequest { Logger = new TestLogger(), ProjectKey = projectKey }; - - return testSubject; - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs deleted file mode 100644 index ddfb5ad8d4..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestTests.cs +++ /dev/null @@ -1,466 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20 -{ - [TestClass] - public class GetIssuesRequestTests - { - [TestMethod] - public async Task InvokeAsync_FilePathNormalized() - { - const string projectKey = "myproject"; - const string statusesToRequest = "some status"; - const string expectedEscapedStatusesInRequest = "some+status"; - - var testSubject = CreateTestSubject(projectKey, statusesToRequest); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - // the contents of the json below were left untouched during the cleanup of the taint related methods & requests of SonarQube.Client - // because they still represent a valid issue format (with flows and secondary locations), - // while the fact that the contents have mentions of taint in them is irrelevant for this test - var request = $"api/issues/search?projects={projectKey}&statuses={expectedEscapedStatusesInRequest}&p=1&ps=500"; - const string response = @" -{ - ""total"": 1, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""effortTotal"": 30, - ""debtTotal"": 30, - ""issues"": [ - { - ""key"": ""AXNZHuA7uQ67pPQjI7e7"", - ""rule"": ""roslyn.sonaranalyzer.security.cs:S5146"", - ""severity"": ""BLOCKER"", - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""project"": ""myprojectkey"", - ""line"": 43, - ""hash"": ""d8684fca55d4dc80e444a993de15ba18"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 19, - ""endOffset"": 43 - }, - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not perform redirects based on tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""rita-g-sonarsource@github"", - ""author"": ""rita.gorokhod@sonarsource.com"", - ""tags"": [], - ""creationDate"": ""2020-07-16T21:31:25+0200"", - ""updateDate"": ""2020-07-16T21:34:05+0200"", - ""type"": ""VULNERABILITY"", - ""organization"": ""myorganization"", - ""fromHotspot"": false - } - ], - ""components"": [ - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""uuid"": ""AXNZHtnVuQ67pPQjI7ey"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WeatherForecastController.cs"", - ""longName"": ""projectroot/Controllers/WeatherForecastController.cs"", - ""path"": ""projectroot/Controllers/WeatherForecastController.cs"" - } - ], - ""organizations"": [ - { - ""key"": ""myorganization"", - ""name"": ""a user"" - } - ], - ""facets"": [] -} -"; - - SetupHttpRequest(handlerMock, request, response); - - var results = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - results.Should().ContainSingle(); - - var result = results[0]; - result.FilePath.Should().Be("projectroot\\Controllers\\WeatherForecastController.cs"); - } - - [TestMethod] - public async Task InvokeAsync_ResponseWithFlows_IsDeserializedCorrectly() - { - const string projectKey = "myproject"; - const string statusesToRequest = "some status"; - const string expectedEscapedStatusesInRequest = "some+status"; - - var testSubject = CreateTestSubject(projectKey, statusesToRequest); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - // the contents of the json below were left untouched during the cleanup of the taint related methods & requests of SonarQube.Client - // because they still represent a valid issue format (with flows and secondary locations), - // while the fact that the contents have mentions of taint in them is irrelevant for this test - var request = $"api/issues/search?projects={projectKey}&statuses={expectedEscapedStatusesInRequest}&p=1&ps=500"; - const string response = @" -{ - ""total"": 1, - ""p"": 1, - ""ps"": 100, - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""effortTotal"": 30, - ""debtTotal"": 30, - ""issues"": [ - { - ""key"": ""AXNZHuA7uQ67pPQjI7e7"", - ""rule"": ""roslyn.sonaranalyzer.security.cs:S5146"", - ""severity"": ""BLOCKER"", - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""project"": ""myprojectkey"", - ""line"": 43, - ""hash"": ""d8684fca55d4dc80e444a993de15ba18"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 19, - ""endOffset"": 43 - }, - ""flows"": [ - { - ""locations"": [ - { - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""textRange"": { - ""startLine"": 43, - ""endLine"": 43, - ""startOffset"": 19, - ""endOffset"": 43 - }, - ""msg"": ""sink: tainted value is used to perform a security-sensitive operation"" - }, - { - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 16, - ""endOffset"": 58 - }, - ""msg"": ""tainted value is propagated"" - }, - { - ""component"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""textRange"": { - ""startLine"": 41, - ""endLine"": 41, - ""startOffset"": 28, - ""endOffset"": 58 - }, - ""msg"": ""tainted value is propagated"" - } - ] - }, - { - ""locations"": [ - { - ""component"": ""myprojectkey:projectroot/Controllers/Helper.cs"", - ""textRange"": { - ""startLine"": 7, - ""endLine"": 7, - ""startOffset"": 12, - ""endOffset"": 29 - }, - ""msg"": ""tainted value is propagated"" - }, - { - ""component"": ""myprojectkey:projectroot/Controllers/Helper.cs"", - ""textRange"": { - ""startLine"": 5, - ""endLine"": 5, - ""startOffset"": 29, - ""endOffset"": 41 - }, - ""msg"": ""tainted value is propagated"" - } - ], - } - ], - ""status"": ""OPEN"", - ""message"": ""Refactor this code to not perform redirects based on tainted, user-controlled data."", - ""effort"": ""30min"", - ""debt"": ""30min"", - ""assignee"": ""rita-g-sonarsource@github"", - ""author"": ""rita.gorokhod@sonarsource.com"", - ""tags"": [], - ""creationDate"": ""2020-07-16T21:31:25+0200"", - ""updateDate"": ""2020-07-16T21:34:05+0200"", - ""type"": ""VULNERABILITY"", - ""organization"": ""myorganization"", - ""fromHotspot"": false - } - ], - ""components"": [ - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey:projectroot/Controllers/WeatherForecastController.cs"", - ""uuid"": ""AXNZHtnVuQ67pPQjI7ey"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""WeatherForecastController.cs"", - ""longName"": ""projectroot/Controllers/WeatherForecastController.cs"", - ""path"": ""projectroot/Controllers/WeatherForecastController.cs"" - }, - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey:projectroot/Controllers/Helper.cs"", - ""uuid"": ""AXNZHtnVuQ67pPQjI7ez"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Helper.cs"", - ""longName"": ""projectroot/Controllers/Helper.cs"", - ""path"": ""projectroot/Controllers/Helper.cs"" - }, - { - ""organization"": ""myorganization"", - ""key"": ""myprojectkey"", - ""uuid"": ""AXJLrCxxxeWiK2BCzDif"", - ""enabled"": true, - ""qualifier"": ""TRK"", - ""name"": ""sanity-connected"", - ""longName"": ""sanity-connected"" - } - ], - ""organizations"": [ - { - ""key"": ""myorganization"", - ""name"": ""a user"" - } - ], - ""facets"": [] -} -"; - - SetupHttpRequest(handlerMock, request, response); - - var results = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - results.Should().ContainSingle(); - - var result = results[0]; - - result.RuleId.Should().Be("roslyn.sonaranalyzer.security.cs:S5146"); - result.Flows.Count().Should().Be(2); - - result.Flows[0].Locations.Count().Should().Be(3); - result.Flows[1].Locations.Count().Should().Be(2); - - var firstFlowFirstLocation = results[0].Flows[0].Locations[0]; - firstFlowFirstLocation.ModuleKey.Should().Be("myprojectkey:projectroot/Controllers/WeatherForecastController.cs"); - firstFlowFirstLocation.FilePath.Should().Be("projectroot\\Controllers\\WeatherForecastController.cs"); - firstFlowFirstLocation.Message.Should().Be("sink: tainted value is used to perform a security-sensitive operation"); - firstFlowFirstLocation.TextRange.StartLine.Should().Be(43); - firstFlowFirstLocation.TextRange.EndLine.Should().Be(43); - firstFlowFirstLocation.TextRange.StartOffset.Should().Be(19); - firstFlowFirstLocation.TextRange.EndOffset.Should().Be(43); - - var seconFlowSecondLocation = results[0].Flows[1].Locations[1]; - seconFlowSecondLocation.ModuleKey.Should().Be("myprojectkey:projectroot/Controllers/Helper.cs"); - seconFlowSecondLocation.FilePath.Should().Be("projectroot\\Controllers\\Helper.cs"); - seconFlowSecondLocation.Message.Should().Be("tainted value is propagated"); - seconFlowSecondLocation.TextRange.StartLine.Should().Be(5); - seconFlowSecondLocation.TextRange.EndLine.Should().Be(5); - seconFlowSecondLocation.TextRange.StartOffset.Should().Be(29); - seconFlowSecondLocation.TextRange.EndOffset.Should().Be(41); - } - - [TestMethod] - [DataRow("")] - [DataRow(null)] - public async Task InvokeAsync_BranchIsNotSpecified_BranchIsNotIncludedInQueryString(string emptyBranch) - { - var testSubject = CreateTestSubject("any", "any", emptyBranch); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // Branch is null/empty => should not be passed - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("branch").Should().BeFalse(); - } - - [TestMethod] - public async Task InvokeAsync_BranchIsSpecified_BranchIsIncludedInQueryString() - { - const string requestedBranch = "mybranch"; - - var testSubject = CreateTestSubject("any", "any", requestedBranch); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // Branch is not null/empty => should be passed - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains($"&branch={requestedBranch}&").Should().BeTrue(); - } - - [TestMethod] - public async Task InvokeAsync_IssueKeysAreNotSpecified_IssueKeysAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", issueKeys: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("issues").Should().BeFalse(); - } - - [TestMethod] - public async Task InvokeAsync_IssueKeysAreSpecified_IssueKeysAreIncludedInQueryString() - { - var issueKeys = new[] { "issue1", "issue2" }; - var testSubject = CreateTestSubject("any", "any", issueKeys: issueKeys); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("issues=issue1%2Cissue2").Should().BeTrue(); - } - - [TestMethod] - public async Task InvokeAsync_RuleIdNotSpecified_RulesAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", ruleId: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("rules").Should().BeFalse(); - } - - [TestMethod] - public async Task InvokeAsync_RuleIdSpecified_RulesAreIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", ruleId: "rule1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Contains("rules=rule1").Should().BeTrue(); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeyNotSpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeySpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: "project1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri(ValidBaseAddress) }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - private static GetIssuesRequest CreateTestSubject( - string projectKey, - string statusesToRequest, - string branch = null, - string[] issueKeys = null, - string ruleId = null, - string componentKey = null) - { - var testSubject = new GetIssuesRequest - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - }; - - return testSubject; - } - - private static string GetSingleActualQueryString(Mock handlerMock) - { - handlerMock.Invocations.Count.Should().Be(1); - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[0].Arguments[0]; - return requestMessage.RequestUri.Query; - } - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs deleted file mode 100644 index 9c569b1954..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesRequestWrapperTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using Moq; -using SonarQube.Client.Api; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20; - -[TestClass] -public class GetIssuesRequestWrapperTests -{ - private const string ComponentPropertyNameSonarQube = "components"; - private const string ComponentPropertyNameSonarCloud = "componentKeys"; - - [DataTestMethod] - [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] - [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] - public async Task InvokeAsync_NoIssueKeys_ExpectedPropertiesArePassedInMultipleRequests(string componentPropertyName) - { - var testSubject = CreateTestSubject(componentPropertyName, "aaaProject", "xStatus", "yBranch", null, "rule1", "component1"); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // The wrapper is expected to make three calls, for code smells, bugs, then vulnerabilities - handlerMock.Invocations.Count.Should().Be(2); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedTypes: "CODE_SMELL"); - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 1, expectedTypes: "BUG"); - } - - [DataTestMethod] - [DataRow(ComponentPropertyNameSonarQube, DisplayName = "SonarQube")] - [DataRow(ComponentPropertyNameSonarCloud, DisplayName = "SonarCloud")] - public async Task InvokeAsync_HasIssueKeys_ExpectedPropertiesArePassedInASingleRequest(string componentPropertyName) - { - var issueKeys = new[] { "issue1", "issue2" }; - var testSubject = CreateTestSubject(componentPropertyName,"aaaProject", "xStatus", "yBranch", issueKeys, "rule1", "component1"); - - var handlerMock = new Mock(MockBehavior.Strict); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - // The wrapper is expected to make one call with the given issueKeys - handlerMock.Invocations.Count.Should().Be(1); - - CheckExpectedQueryStringsParameters(componentPropertyName, handlerMock, 0, expectedKeys: issueKeys); - } - - private static IGetIssuesRequest CreateTestSubject(string componentPropertyName, string projectKey, string statusesToRequest, string branch, string[] issueKeys, string ruleId, string componentKey) - { - return componentPropertyName switch - { - ComponentPropertyNameSonarQube => new GetIssuesRequestWrapper - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - ComponentKey = componentKey - }, - ComponentPropertyNameSonarCloud => new GetIssuesRequestWrapper - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - Branch = branch, - IssueKeys = issueKeys, - RuleId = ruleId, - ComponentKey = componentKey - }, - _ => throw new ArgumentOutOfRangeException() - }; - } - - private static void CheckExpectedQueryStringsParameters(string componentKeyName, - Mock handlerMock, - int invocationIndex, - string expectedTypes = null, - string[] expectedKeys = null) - { - var actualQueryString = GetActualQueryStringForInvocation(handlerMock, invocationIndex); - - Console.WriteLine($"Invocation [{invocationIndex}]: {actualQueryString}"); - actualQueryString.Contains($"?{componentKeyName}=component1").Should().BeTrue(); - actualQueryString.Contains("&projects=aaaProject").Should().BeTrue(); - actualQueryString.Contains("&statuses=xStatus").Should().BeTrue(); - actualQueryString.Contains("&branch=yBranch").Should().BeTrue(); - actualQueryString.Contains("&rules=rule1").Should().BeTrue(); - - if (expectedTypes != null) - { - actualQueryString.Contains($"&types={expectedTypes}").Should().BeTrue(); - } - else - { - actualQueryString.Contains("types").Should().BeFalse(); - } - - if (expectedKeys != null) - { - var keys = string.Join("%2C", expectedKeys); - actualQueryString.Contains($"&issues={keys}").Should().BeTrue(); - } - else - { - actualQueryString.Contains("issues").Should().BeFalse(); - } - - } - - private static string GetActualQueryStringForInvocation(Mock handlerMock, int invocationIndex) - { - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[invocationIndex].Arguments[0]; - return requestMessage.RequestUri.Query; - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs deleted file mode 100644 index 28b62b3354..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarCloudRequestTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20; - -[TestClass] -public class GetIssuesWithComponentSonarCloudRequestTests -{ - [TestMethod] - public async Task InvokeAsync_ComponentKeyNotSpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeySpecified_ComponentsAreIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: "project1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().Contain("componentKeys=project1"); - } - - private static GetIssuesWithComponentSonarCloudRequest CreateTestSubject(string projectKey, string statusesToRequest, string componentKey = null) - { - var testSubject = new GetIssuesWithComponentSonarCloudRequest - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - ComponentKey = componentKey - }; - - return testSubject; - } - - private static string GetSingleActualQueryString(Mock handlerMock) - { - handlerMock.Invocations.Count.Should().Be(1); - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[0].Arguments[0]; - return requestMessage.RequestUri.Query; - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs deleted file mode 100644 index 8e4d636bbd..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V7_20/GetIssuesWithComponentSonarQubeRequestTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests.Requests.Api.V7_20; - -[TestClass] -public class GetIssuesWithComponentSonarQubeRequestTests -{ - [TestMethod] - public async Task InvokeAsync_ComponentKeyNotSpecified_ComponentsAreNotIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: null); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().NotContain("component"); - } - - [TestMethod] - public async Task InvokeAsync_ComponentKeySpecified_ComponentsAreIncludedInQueryString() - { - var testSubject = CreateTestSubject("any", "any", componentKey: "project1"); - - var handlerMock = new Mock(); - var httpClient = new HttpClient(handlerMock.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - - SetupHttpRequest(handlerMock, EmptyGetIssuesResponse); - _ = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - - var actualQueryString = GetSingleActualQueryString(handlerMock); - actualQueryString.Should().Contain("components=project1"); - } - - private static GetIssuesWithComponentSonarQubeRequest CreateTestSubject(string projectKey, string statusesToRequest, string componentKey = null) - { - var testSubject = new GetIssuesWithComponentSonarQubeRequest - { - Logger = new TestLogger(), - ProjectKey = projectKey, - Statuses = statusesToRequest, - ComponentKey = componentKey - }; - - return testSubject; - } - - private static string GetSingleActualQueryString(Mock handlerMock) - { - handlerMock.Invocations.Count.Should().Be(1); - var requestMessage = (HttpRequestMessage)handlerMock.Invocations[0].Arguments[0]; - return requestMessage.RequestUri.Query; - } -} diff --git a/src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs b/src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs deleted file mode 100644 index 75571c303d..0000000000 --- a/src/SonarQube.Client.Tests/Requests/Api/V9_4/GetSonarLintEventStreamTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using Moq; -using SonarQube.Client.Api.V9_4; -using SonarQube.Client.Logging; -using SonarQube.Client.Tests.Infra; - -namespace SonarQube.Client.Tests.Requests.Api.V9_4; - -[TestClass] -public class GetSonarLintEventStreamTests -{ - [TestMethod] - public async Task InvokeAsync_ReturnsCorrectStream() - { - using var testedStream = new MemoryStream(Encoding.UTF8.GetBytes("hello this is a test")); - var messageHandler = new Mock(); - using var httpClient = new HttpClient(messageHandler.Object) { BaseAddress = new Uri("http://localhost") }; - - MocksHelper.SetupHttpRequest( - messageHandler, - requestRelativePath: "api/push/sonarlint_events?languages=cs%2Cvbnet&projectKeys=someproj", - responseMessage: new HttpResponseMessage { Content = new StreamContent(testedStream) }, - headers: MediaTypeHeaderValue.Parse("text/event-stream")); - - var testSubject = new GetSonarLintEventStream { ProjectKey = "someproj", Logger = Mock.Of() }; - - using var response = await testSubject.InvokeAsync(httpClient, CancellationToken.None); - response.Should().NotBeNull(); - messageHandler.VerifyAll(); - - using var reader = new StreamReader(response, Encoding.UTF8); - var responseString = await reader.ReadToEndAsync(); - responseString.Should().Be("hello this is a test"); - } -} diff --git a/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs b/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs index 1a405e82c7..9b8bc25600 100644 --- a/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs +++ b/src/SonarQube.Client.Tests/Requests/DefaultConfiguration_Configure_Tests.cs @@ -35,19 +35,9 @@ public void ConfigureSonarQube_Writes_Debug_Messages() var expected = new[] { "Registered SonarQube.Client.Api.V2_10.GetVersionRequest for 2.1", - "Registered SonarQube.Client.Api.V2_60.GetPropertiesRequest for 2.6", "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest for 3.3", - "Registered SonarQube.Client.Api.V5_20.GetQualityProfilesRequest for 5.2", - "Registered SonarQube.Client.Api.V5_50.GetRulesRequest for 5.5", - "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest for 6.3", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest for 6.5", "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest for 6.6", "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest for 6.6", - "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarQubeRequest] for 7.2", - "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest for 7.2", - "Registered SonarQube.Client.Api.V9_4.GetSonarLintEventStream for 9.4", - "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest for 10.2", - "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest for 9.9", }; DefaultConfiguration.ConfigureSonarQube(new RequestFactory(logger)); @@ -67,14 +57,8 @@ public void ConfigureSonarCloud_Writes_Debug_Messages() { "Registered SonarQube.Client.Api.V2_10.GetVersionRequest", "Registered SonarQube.Client.Api.V3_30.ValidateCredentialsRequest", - "Registered SonarQube.Client.Api.V10_2.GetRulesWithCCTRequest", - "Registered SonarQube.Client.Api.V6_30.GetPropertiesRequest", - "Registered SonarQube.Client.Api.V6_50.GetQualityProfilesRequest", "Registered SonarQube.Client.Api.V6_60.GetNotificationsRequest", "Registered SonarQube.Client.Api.V6_60.GetProjectBranchesRequest", - "Registered SonarQube.Client.Api.V7_20.GetIssuesRequestWrapper`1[SonarQube.Client.Api.V7_20.GetIssuesWithComponentSonarCloudRequest]", - "Registered SonarQube.Client.Api.V7_20.GetExclusionsRequest", - "Registered SonarQube.Client.Api.V9_9.SearchFilesByNameRequest" }; DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(logger)); @@ -91,17 +75,10 @@ public void ConfigureSonarQube_CheckAllRequestsImplemented() var testSubject = DefaultConfiguration.ConfigureSonarQube(new RequestFactory(new TestLogger())); var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); } [TestMethod] @@ -110,28 +87,10 @@ public void ConfigureSonarCloud_CheckAllRequestsImplemented() var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); - } - - [TestMethod] - [Description("The following APIs are not implemented on SC (yet). Verify that they are not registered in the factory.")] - public void ConfigureSonarCloud_CheckUnsupportedRequestsAreNotImplemented() - { - var testSubject = DefaultConfiguration.ConfigureSonarCloud(new UnversionedRequestFactory(new TestLogger())); - var serverInfo = new ServerInfo(null /* latest */, ServerType.SonarQube); - - Action act = () => testSubject.Create(serverInfo); - act.Should().Throw().And.Message.Should() - .Be("Could not find factory for 'IGetSonarLintEventStream'."); } private static void DumpDebugMessages(TestLogger logger) diff --git a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj index ff3522c5fd..67ca3fdf5a 100644 --- a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj +++ b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj @@ -30,4 +30,14 @@ + + + TestParallelization.cs + + + + + + + \ No newline at end of file diff --git a/src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs b/src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs deleted file mode 100644 index 862af07d1a..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_CreateServerSentEventsSession.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using Moq; -using SonarQube.Client.Models.ServerSentEvents; - -namespace SonarQube.Client.Tests; - -[TestClass] -public class SonarQubeService_CreateSSEStreamReader : SonarQubeService_TestBase -{ - [TestMethod] - public async Task CreateSSEStreamReader_SupportedServerVersion_CreatesSession() - { - var expectedCreatedSSEStream = Mock.Of(); - - sseStreamFactory - .Setup(x => x.Create(It.IsAny(), CancellationToken.None)) - .Returns(expectedCreatedSSEStream); - - await ConnectToSonarQube("9.4.0.0"); - - SetupRequest("api/push/sonarlint_events?languages=cs%2Cvbnet&projectKeys=myProject", - new HttpResponseMessage - { - Content = new StreamContent(Stream.Null) - }, - MediaTypeHeaderValue.Parse("text/event-stream")); - - var result = await service.CreateSSEStreamReader("myProject", CancellationToken.None); - - result.Should().NotBeNull(); - result.Should().Be(expectedCreatedSSEStream); - } - - [TestMethod] - public async Task CreateSSEStreamReader_UnsupportedServerVersion_InvalidOperationException() - { - await ConnectToSonarQube("3.3.0.0"); - - Func> func = async () => await service.CreateSSEStreamReader("myProject", CancellationToken.None); - - const string expectedErrorMessage = - "Could not find compatible implementation of 'IGetSonarLintEventStream' for SonarQube 3.3.0.0."; - - func.Should().ThrowExactly().WithMessage(expectedErrorMessage); - - logger.ErrorMessages.Should().Contain(expectedErrorMessage); - } - - [TestMethod] - public void CreateSSEStreamReader_NotConnected_InvalidOperationException() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func> func = async () => await service.CreateSSEStreamReader("myProject", CancellationToken.None); - - func.Should().ThrowExactly() - .WithMessage("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs deleted file mode 100644 index 3b995a8d88..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetIssuesBase.cs +++ /dev/null @@ -1,172 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using FluentAssertions; -using SonarQube.Client.Api; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Tests -{ - public class SonarQubeService_GetIssuesBase : SonarQubeService_TestBase - { - protected const int MaxAllowedIssues = PagedRequestBase.MaximumItemsCount; - protected const int PageSize = PagedRequestBase.MaximumPageSize; - - protected void SetupPageOfResponses(string projectName, int pageNumber, int numberOfIssues, string issueType, string[] languages=null) - { - // Sanity check of the issue types - issueType.Should().BeOneOf("CODE_SMELL", "BUG", "VULNERABILITY"); - - var startItemNumber = (pageNumber - 1) * PageSize + 1; - - var issuesJson = string.Empty; - var componentJson = string.Empty; - if (numberOfIssues > 0) - { - issuesJson = string.Join(",\n", Enumerable.Range(startItemNumber, numberOfIssues).Select(CreateIssueJson)); - componentJson = CreateComponentJson(); - } - - var languagesParameter = languages is null ? string.Empty : "&languages=" + string.Join("%2C", languages); - - SetupRequest($"api/issues/search?projects={projectName}&statuses=RESOLVED&types={issueType}{languagesParameter}&p={pageNumber}&ps={PageSize}", $@" -{{ - ""paging"": {{ - ""pageIndex"": {pageNumber}, - ""pageSize"": {PageSize}, - ""total"": 9999 - }}, - ""issues"": [ - {issuesJson} - ], - ""components"": [ - {componentJson} - ] -}}"); - } - - protected void SetupPageOfResponses(string projectName, string ruleId, string componentKey, string branch, int pageNumber, int numberOfIssues, string issueType, string[] languages=null) - { - // Sanity check of the issue types - issueType.Should().BeOneOf("CODE_SMELL", "BUG", "VULNERABILITY"); - - var startItemNumber = (pageNumber - 1) * PageSize + 1; - - var issuesJson = string.Empty; - var componentJson = string.Empty; - if (numberOfIssues > 0) - { - issuesJson = string.Join(",\n", Enumerable.Range(startItemNumber, numberOfIssues).Select(CreateIssueJson)); - componentJson = CreateComponentJson(); - } - - var languagesParameter = languages is null ? string.Empty : "&languages=" + string.Join("%2C", languages); - - SetupRequest($"api/issues/search?components={componentKey}&projects={projectName}&rules={ruleId}&branch={branch}&types={issueType}{languagesParameter}&p={pageNumber}&ps={PageSize}", $@" -{{ - ""paging"": {{ - ""pageIndex"": {pageNumber}, - ""pageSize"": {PageSize}, - ""total"": 9999 - }}, - ""issues"": [ - {issuesJson} - ], - ""components"": [ - {componentJson} - ] -}}"); - } - - protected void SetupPagesOfResponses(string projectName, int numberOfIssues, string issueType, string[] languages = null) - { - var pageNumber = 1; - var remainingIssues = numberOfIssues; - while (remainingIssues > 0) - { - var issuesOnNewPage = Math.Min(remainingIssues, PageSize); - SetupPageOfResponses(projectName, pageNumber, issuesOnNewPage, issueType, languages); - - pageNumber++; - remainingIssues -= issuesOnNewPage; - } - } - - protected void SetupPagesOfResponses(string projectName, string ruleId, string componentKey, string branch, int numberOfIssues, string issueType) - { - var pageNumber = 1; - var remainingIssues = numberOfIssues; - while (remainingIssues > 0) - { - var issuesOnNewPage = Math.Min(remainingIssues, PageSize); - SetupPageOfResponses(projectName, ruleId, componentKey, branch, pageNumber, issuesOnNewPage, issueType); - - pageNumber++; - remainingIssues -= issuesOnNewPage; - } - } - - protected static string CreateIssueJson(int number) => - "{ " + - $"\"key\": \"{number}\", " + - "\"rule\": \"csharpsquid:S1075\", " + - "\"component\": \"shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs\", " + - "\"project\": \"shared\", " + - "\"subProject\": \"shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082\", " + - "\"line\": 36, " + - "\"hash\": \"0dcbf3b077bacc9fdbd898ff3b587085\", " + - "\"status\": \"RESOLVED\", " + - "\"message\": \"Refactor your code not to use hardcoded absolute paths or URIs.\" " + - "}"; - - protected static string CreateComponentJson() => - // "key" should be the same as the "component" field in the issues from CreateIssueJson() - @" -{ - ""organization"": ""default-organization"", - ""key"": ""shared:shared:2B470B7D-D47B-4E41-B105-D3938E196082:Program.cs"", - ""uuid"": ""AWg8adNk_JurIR2zdSvM"", - ""enabled"": true, - ""qualifier"": ""FIL"", - ""name"": ""Program.cs"", - ""longName"": ""Program.cs"", - ""path"": ""Program.cs"" -}"; - - protected void checkForExpectedWarning(int itemCount, string partialText) - { - // Only expect a warning if the number of items is equal or greater than the max allowed - var expectedMessageCount = itemCount >= MaxAllowedIssues ? 1 : 0; - logger.WarningMessages.Count(x => x.Contains(partialText)).Should().Be(expectedMessageCount); - } - - protected void DumpWarningsToConsole() - { - System.Console.WriteLine("Warnings:"); - foreach (string item in logger.WarningMessages) - { - System.Console.WriteLine(item); - } - System.Console.WriteLine(""); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs b/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs index 07005926c0..77a48635f6 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_Miscellaneous.cs @@ -44,9 +44,6 @@ public void SonarQubeService_Ctor_ArgumentChecks() action = () => new SonarQubeService(string.Empty, null, languageProvider); action.Should().ThrowExactly().And.ParamName.Should().Be("logger"); - - action = () => new SonarQubeService(string.Empty, logger, null); - action.Should().ThrowExactly().And.ParamName.Should().Be("languageProvider"); } [TestMethod] @@ -81,7 +78,7 @@ public async Task SonarQubeService_UsesFactorySelector(string serverUrl, bool is var messageHandlerFactory = new Mock(); messageHandlerFactory.Setup(x => x.Create(connectionInfo.ServerUri)).Returns(Mock.Of()); - var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, Substitute.For(), selectorMock.Object, null); + var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, selectorMock.Object); await testSubject.ConnectAsync(connectionInfo, CancellationToken.None); selectorMock.Verify(x => x.Select(isSonarCloud, logger), Times.Once); @@ -111,7 +108,7 @@ public async Task SonarQubeService_FactorySelector_RequestsNewFactoryOnEachConne var messageHandlerFactory = new Mock(); messageHandlerFactory.Setup(x => x.Create(It.IsAny())).Returns(Mock.Of()); - var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, Substitute.For(), selectorMock.Object, null); + var testSubject = new SonarQubeService(messageHandlerFactory.Object, "user-agent", logger, selectorMock.Object); // 1. Connect to SonarQube await testSubject.ConnectAsync(sonarQubeConnectionInfo, CancellationToken.None); diff --git a/src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs b/src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs deleted file mode 100644 index e589cad08a..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_PagedRequestTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Requests; -using SonarQube.Client.Tests.Infra; -using static SonarQube.Client.Tests.Infra.MocksHelper; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_PagedRequestTests - { - private Mock messageHandler; - private TestLogger logger; - private HttpClient client; - - [TestInitialize] - public void TestInitialize() - { - messageHandler = new Mock(MockBehavior.Strict); - client = new HttpClient(messageHandler.Object) - { - BaseAddress = new Uri(ValidBaseAddress) - }; - logger = new TestLogger(); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task MaxItemCount_RetrievesTheSpecifiedNumberOfPages(bool shouldLimit) - { - var request = new DummyPagedRequest - { - Logger = logger, - }; - - if (shouldLimit) - { - request.ItemsLimit = 1100; - } - - SetupRequest("api/dummy?p=1&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 1, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1, 500).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - SetupRequest("api/dummy?p=2&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 2, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(500, 500).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - SetupRequest("api/dummy?p=3&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 3, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1000, 500).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - SetupRequest("api/dummy?p=4&ps=500", $@" -{{ - ""paging"": {{ - ""pageIndex"": 3, - ""pageSize"": 4, - ""total"": 1000 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1500, 100).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - var expectedCount = shouldLimit ? 1100 : 1600; - - var result = await request.InvokeAsync(client, CancellationToken.None); - result.Should().HaveCount(expectedCount); - } - - [TestMethod] - public async Task PageSize_IsRespected() - { - var request = new DummyPagedRequest - { - Logger = logger, - PageSize = 22 - }; - - // If the correct page size isn't passed in the query string then the - // test will fail because the mock http client won't have a response - // matches the supplied URL. - SetupRequest("api/dummy?p=1&ps=22", @" -{ - ""dummyResponses"": [ - { - ""key"": ""cs"", - ""name"": ""C#"", - }, - { - ""key"": ""vbnet"", - ""name"": ""VB.NET"" - } - ] -}"); - var result = await request.InvokeAsync(client, CancellationToken.None); - result.Should().HaveCount(2); - } - - [TestMethod] - public async Task RequestPageSize_IsRespected() - { - var request = new DummyPagedRequest - { - Logger = logger, - PageSize = 4 - }; - - // Full page of data - SetupRequest("api/dummy?p=1&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 1, - ""pageSize"": 4, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(1, 4).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - // Full page of data - SetupRequest("api/dummy?p=2&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 2, - ""pageSize"": 4, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(5, 4).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - // Partial page of data - should stop - SetupRequest("api/dummy?p=3&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 3, - ""pageSize"": 2, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(9, 2).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - // Additional page - should never be requested - SetupRequest("api/dummy?p=4&ps=4", $@" -{{ - ""paging"": {{ - ""pageIndex"": 4, - ""pageSize"": 2, - ""total"": 10 - }}, - ""dummyResponses"": [ - {string.Join(",\n", Enumerable.Range(11, 2).Select(i => $@"{{ ""key"": ""{i}"", ""name"": ""Name{i}"" }}"))} - ] -}}"); - - var result = await request.InvokeAsync(client, CancellationToken.None); - - // Should stop after two pages of data - result.Should().HaveCount(10); - } - - private void SetupRequest(string relativePath, string response, HttpStatusCode statusCode = HttpStatusCode.OK) => - SetupHttpRequest(messageHandler, relativePath, response, statusCode, ValidBaseAddress); - - #region Dummy request and response objects - - private class DummyPagedRequest : PagedRequestBase - { - protected override string Path => "api/dummy"; - - protected override DummyObject[] ParseResponse(string response) => - JObject.Parse(response)["dummyResponses"] - .ToObject() - .Select(ToDummyObject) - .ToArray(); - - - private DummyObject ToDummyObject(DummyResponse dummyResponse) => - new DummyObject(dummyResponse.Key, dummyResponse.Name); - - private class DummyResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - } - } - - /// - /// Data object returned by the request - /// - private class DummyObject - { - public DummyObject(string key, string name) - { - Key = key; - Name = name; - } - - public string Key { get; } - public string Name { get; } - } - - #endregion - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs b/src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs deleted file mode 100644 index 98f593c456..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_SearchFilesByName.cs +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_SearchFilesByName : SonarQubeService_TestBase - { - [TestMethod] - public void SearchFilesByName_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func action = async () => await service.SearchFilesByNameAsync("projectKey", "branch", "fileName", CancellationToken.None); - - action.Should().ThrowExactly() - .WithMessage("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - - [TestMethod] - public async Task SearchFilesByName_WrongBranch() - { - await ConnectToSonarQube("9.9.0.0"); - - var branch = "branch"; - var projectKey = "projectKey"; - var fileName = "fileName.cs"; - - var request = $"api/components/tree?component={projectKey}&branch={branch}&q={fileName}&qualifiers=FIL%2CUTS&p=1&ps=500"; - - var response = "{\r\n \"errors\": [\r\n {\r\n \"msg\": \"Component 'xyz' on branch 'none' not found\"\r\n }\r\n ]\r\n}"; - - SetupRequest(request, response, HttpStatusCode.NotFound); - - var func = async () => await service.SearchFilesByNameAsync(projectKey, branch, fileName, CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public async Task SearchFilesByName_SingleFile_Returns() - { - await ConnectToSonarQube("9.9.0.0"); - - var branch = "master"; - var projectKey = "sonarlint-visualstudio"; - var fileName = "LocalHotspotStoreTests.cs"; - - var request = $"api/components/tree?component={projectKey}&branch={branch}&q={fileName}&qualifiers=FIL%2CUTS&p=1&ps=500"; - - var response = @"{ - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""baseComponent"": { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio"", - ""name"": ""SonarLint for Visual Studio"", - ""qualifier"": ""TRK"", - ""tags"": [ - ""dotnet"" - ], - ""visibility"": ""public"" - }, - ""components"": [ - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:src/IssueViz.Security.UnitTests/Hotspots/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""src/IssueViz.Security.UnitTests/Hotspots/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - } - ] -}"; - SetupRequest(request, response); - - var filePaths = await service.SearchFilesByNameAsync(projectKey, branch, fileName, CancellationToken.None); - - filePaths.Should().ContainSingle(); - filePaths.Should().HaveElementAt(0, @"src\IssueViz.Security.UnitTests\Hotspots\LocalHotspotStoreTests.cs"); - } - - [TestMethod] - public async Task SearchFilesByName_MultipleFiles_Returns() - { - await ConnectToSonarQube("9.9.0.0"); - - var branch = "master"; - var projectKey = "sonarlint-visualstudio"; - var fileName = "LocalHotspotStoreTests.cs"; - - var request = $"api/components/tree?component={projectKey}&branch={branch}&q={fileName}&qualifiers=FIL%2CUTS&p=1&ps=500"; - - var response = @"{ - ""paging"": { - ""pageIndex"": 1, - ""pageSize"": 100, - ""total"": 1 - }, - ""baseComponent"": { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio"", - ""name"": ""SonarLint for Visual Studio"", - ""qualifier"": ""TRK"", - ""tags"": [ - ""dotnet"" - ], - ""visibility"": ""public"" - }, - ""components"": [ - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:path0/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""path0/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - }, - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:path1/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""path1/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - }, - { - ""organization"": ""sonarsource"", - ""key"": ""sonarlint-visualstudio:path2/LocalHotspotStoreTests.cs"", - ""name"": ""LocalHotspotStoreTests.cs"", - ""qualifier"": ""UTS"", - ""path"": ""path2/LocalHotspotStoreTests.cs"", - ""language"": ""cs"" - }, - ] -}"; - SetupRequest(request, response); - - var filePaths = await service.SearchFilesByNameAsync(projectKey, branch, fileName, CancellationToken.None); - - filePaths.Should().HaveCount(3); - filePaths.Should().HaveElementAt(0, @"path0\LocalHotspotStoreTests.cs"); - filePaths.Should().HaveElementAt(1, @"path1\LocalHotspotStoreTests.cs"); - filePaths.Should().HaveElementAt(2, @"path2\LocalHotspotStoreTests.cs"); - } - } -} diff --git a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs index 9984f8b8ea..2cf6a1c209 100644 --- a/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs +++ b/src/SonarQube.Client.Tests/SonarQubeService_TestBase.cs @@ -28,7 +28,6 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Requests; using SonarQube.Client.Tests.Infra; @@ -45,7 +44,6 @@ public class SonarQubeService_TestBase // Note: can't be protected because the interfaces are internal internal IRequestFactorySelector requestFactorySelector; - internal Mock sseStreamFactory; private const string DefaultBasePath = "http://localhost/"; @@ -71,7 +69,6 @@ public void TestInitialize() proxyDetector = new Mock(); requestFactorySelector = new RequestFactorySelector(); - sseStreamFactory = new Mock(); ResetService(); } @@ -127,7 +124,7 @@ protected void ResetService() protected internal virtual SonarQubeService CreateTestSubject() { - return new SonarQubeService(httpClientHandlerFactory.Object, UserAgent, logger, languageProvider, requestFactorySelector, sseStreamFactory.Object); + return new SonarQubeService(httpClientHandlerFactory.Object, UserAgent, logger, requestFactorySelector); } private static IUsernameAndPasswordCredentials MockBasicAuthCredentials(string userName, SecureString password) diff --git a/src/SonarQube.Client/Api/Common/ApiExtensions.cs b/src/SonarQube.Client/Api/Common/ApiExtensions.cs deleted file mode 100644 index 1ac812a4b6..0000000000 --- a/src/SonarQube.Client/Api/Common/ApiExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.Common -{ - internal static class ApiExtensions - { - internal static ILookup GetComponentKeyPathLookup(this JObject root) - { - var components = root["components"] == null - ? Array.Empty() - : root["components"].ToObject(); - - return components - .Where(c => c.IsFile) - .ToLookup(c => c.Key, c => c.Path); // Using a Lookup because it does not throw, unlike the Dictionary - } - - internal static IEnumerable GetComponentPathList(this JObject root) - { - var components = root["components"] == null - ? Array.Empty() - : root["components"].ToObject(); - - return components.Select(c => c.Path); - } - - internal static IssueTextRange ToIssueTextRange(this ServerIssueTextRange serverIssueTextRange) - { - return serverIssueTextRange == null - ? null - : new IssueTextRange(serverIssueTextRange.StartLine, - serverIssueTextRange.EndLine, - serverIssueTextRange.StartOffset, - serverIssueTextRange.EndOffset); - } - } -} diff --git a/src/SonarQube.Client/Api/Common/ServerComponent.cs b/src/SonarQube.Client/Api/Common/ServerComponent.cs deleted file mode 100644 index fa776b3de8..0000000000 --- a/src/SonarQube.Client/Api/Common/ServerComponent.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; - -namespace SonarQube.Client.Api.Common -{ - internal sealed class ServerComponent - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("qualifier")] - public string Qualifier { get; set; } - - [JsonProperty("path")] - public string Path { get; set; } - - public bool IsFile - { - get { return Qualifier == "FIL"; } - } - } -} diff --git a/src/SonarQube.Client/Api/Common/ServerImpact.cs b/src/SonarQube.Client/Api/Common/ServerImpact.cs deleted file mode 100644 index ce8d3af5c7..0000000000 --- a/src/SonarQube.Client/Api/Common/ServerImpact.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; - -namespace SonarQube.Client.Api.Common -{ - internal class ServerImpact - { - [JsonProperty("softwareQuality")] - public string SoftwareQuality { get; set; } - - [JsonProperty("severity")] - public string Severity { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs b/src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs deleted file mode 100644 index 30a71adeab..0000000000 --- a/src/SonarQube.Client/Api/Common/ServerIssueTextRange.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; - -namespace SonarQube.Client.Api.Common -{ - internal sealed class ServerIssueTextRange - { - [JsonProperty("startLine")] - public int StartLine { get; set; } - - [JsonProperty("endLine")] - public int EndLine { get; set; } - - [JsonProperty("startOffset")] - public int StartOffset { get; set; } - - [JsonProperty("endOffset")] - public int EndOffset { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/DefaultConfiguration.cs b/src/SonarQube.Client/Api/DefaultConfiguration.cs index eb742b4d07..ab56496060 100644 --- a/src/SonarQube.Client/Api/DefaultConfiguration.cs +++ b/src/SonarQube.Client/Api/DefaultConfiguration.cs @@ -28,19 +28,9 @@ public static RequestFactory ConfigureSonarQube(RequestFactory requestFactory) { requestFactory .RegisterRequest("2.1") - .RegisterRequest("2.6") .RegisterRequest("3.3") - .RegisterRequest("5.2") - .RegisterRequest("5.5") - .RegisterRequest("6.3") - .RegisterRequest("6.5") .RegisterRequest("6.6") - .RegisterRequest("6.6") - .RegisterRequest>("7.2") - .RegisterRequest("7.2") - .RegisterRequest("9.4") - .RegisterRequest("10.2") - .RegisterRequest("9.9"); + .RegisterRequest("6.6"); return requestFactory; } @@ -50,14 +40,8 @@ public static UnversionedRequestFactory ConfigureSonarCloud(UnversionedRequestFa requestFactory .RegisterRequest() .RegisterRequest() - .RegisterRequest() - .RegisterRequest() - .RegisterRequest() .RegisterRequest() - .RegisterRequest() - .RegisterRequest>() - .RegisterRequest() - .RegisterRequest(); + .RegisterRequest(); return requestFactory; } diff --git a/src/SonarQube.Client/Api/IGetExclusionsRequest.cs b/src/SonarQube.Client/Api/IGetExclusionsRequest.cs deleted file mode 100644 index 13a62385b8..0000000000 --- a/src/SonarQube.Client/Api/IGetExclusionsRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface IGetExclusionsRequest : IRequest - { - string ProjectKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/IGetIssuesRequest.cs b/src/SonarQube.Client/Api/IGetIssuesRequest.cs deleted file mode 100644 index 08ad5c5e55..0000000000 --- a/src/SonarQube.Client/Api/IGetIssuesRequest.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api; - -interface IGetIssuesRequest : IRequest -{ - string ProjectKey { get; set; } - - string Statuses { get; set; } - - /// - /// The branch name to fetch. - /// - /// If the value is null/empty, the main branch will be fetched - string Branch { get; set; } - - string[] IssueKeys { get; set; } - - string RuleId { get; set; } - - string ComponentKey { get; set; } - - string Languages { get; set; } - - // Update when adding properties here. -} diff --git a/src/SonarQube.Client/Api/IGetPropertiesRequest.cs b/src/SonarQube.Client/Api/IGetPropertiesRequest.cs deleted file mode 100644 index 42932eff18..0000000000 --- a/src/SonarQube.Client/Api/IGetPropertiesRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface IGetPropertiesRequest : IRequest - { - string ProjectKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs b/src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs deleted file mode 100644 index eb9e6e35ab..0000000000 --- a/src/SonarQube.Client/Api/IGetQualityProfilesRequest.cs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface IGetQualityProfilesRequest : IRequest - { - string ProjectKey { get; set; } - - string OrganizationKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/IGetRulesRequest.cs b/src/SonarQube.Client/Api/IGetRulesRequest.cs deleted file mode 100644 index 114d1f6ef7..0000000000 --- a/src/SonarQube.Client/Api/IGetRulesRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface IGetRulesRequest : IPagedRequest - { - bool? IsActive { get; set; } - - string QualityProfileKey { get; set; } - - string RuleKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/IGetSonarLintEventStream.cs b/src/SonarQube.Client/Api/IGetSonarLintEventStream.cs deleted file mode 100644 index 9be957d125..0000000000 --- a/src/SonarQube.Client/Api/IGetSonarLintEventStream.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - internal interface IGetSonarLintEventStream : IRequest - { - string Languages { get; set; } - string ProjectKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs b/src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs deleted file mode 100644 index 14d5b66ea5..0000000000 --- a/src/SonarQube.Client/Api/ISearchFilesByNameRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api -{ - public interface ISearchFilesByNameRequest : IPagedRequest - { - string ProjectKey { get; set; } - string BranchName { get; set; } - string FileName { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs b/src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs deleted file mode 100644 index 4eb1a62add..0000000000 --- a/src/SonarQube.Client/Api/V10_2/GetIssuesWithCCTRequest.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Api.V7_20; -using SonarQube.Client.Api.V9_6; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V10_2 -{ - /// - /// This class does not support component-based search. See for more information - /// - internal class GetIssuesWithCCTRequest : GetIssuesWithContextRequest - { - protected override SonarQubeIssue[] ParseResponse(string response) - { - var root = JObject.Parse(response); - - // This is a paged request so ParseResponse will be called once for each "page" - // of the response. However, we expect each page to be self-contained, so we want - // to rebuild the lookup each time. - componentKeyPathLookup = root.GetComponentKeyPathLookup(); - - return root["issues"] - .ToObject() - .Select(ToSonarQubeIssue) - .ToArray(); - } - - private SonarQubeIssue ToSonarQubeIssue(ServerIssueWithCCT issue) => new SonarQubeIssue(issue.IssueKey, - ComputePath(issue.Component), - issue.Hash, - issue.Message, - ComputeModuleKey(issue), - issue.CompositeRuleKey, - issue.Status == "RESOLVED", - SonarQubeIssueSeverityConverter.Convert(issue.Severity), - issue.CreationDate, - issue.UpdateDate, - issue.TextRange.ToIssueTextRange(), - ToIssueFlows(issue.Flows), - issue.ContextKey, - CleanCodeTaxonomyHelpers.ToSonarQubeCleanCodeAttribute(issue.CleanCodeAttribute), - CleanCodeTaxonomyHelpers.ToDefaultImpacts(issue.Impacts)); - - private class ServerIssueWithCCT : ServerIssue - { - [JsonProperty("cleanCodeAttribute")] - public string CleanCodeAttribute { get; set; } - - [JsonProperty("impacts")] - public ServerImpact[] Impacts { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs b/src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs deleted file mode 100644 index 30f3f76ccb..0000000000 --- a/src/SonarQube.Client/Api/V10_2/GetRulesWithCCTRequest.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using SonarQube.Client.Api.V5_50; -using SonarQube.Client.Api.V9_6; -using SonarQube.Client.Logging; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V10_2 -{ - public class GetRulesWithCCTRequest : IGetRulesRequest - { - private static readonly IList ResponseFieldsOverride = GetRulesRequest.ResponseList.Concat(new[] { "cleanCodeAttribute" }).ToArray(); - - private readonly GetRulesRequest innerRequest = new GetRulesRequest(); - - public ILogger Logger { get; set; } - public int Page { get; set; } - public int PageSize { get; set; } - public int ItemsLimit { get; set; } - public bool? IsActive { get; set; } - public string QualityProfileKey { get; set; } - public string RuleKey { get; set; } - - public async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - innerRequest.IsActive = this.IsActive; - innerRequest.QualityProfileKey = this.QualityProfileKey; - innerRequest.RuleKey = this.RuleKey; - innerRequest.Logger = this.Logger; - - innerRequest.ResponseListField = ResponseFieldsOverride; - - return await innerRequest.InvokeAsync(httpClient, token); - } - } -} diff --git a/src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs b/src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs deleted file mode 100644 index 582247f7a4..0000000000 --- a/src/SonarQube.Client/Api/V2_60/GetPropertiesRequest.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using Newtonsoft.Json; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V2_60 -{ - public class GetPropertiesRequest : RequestBase, IGetPropertiesRequest - { - [JsonProperty("resource")] - public string ProjectKey { get; set; } - - protected override string Path => "api/properties"; - - protected override SonarQubeProperty[] ParseResponse(string response) => - JsonHelper.Deserialize(response) - .Select(ToProperty) - .ToArray(); - - private SonarQubeProperty ToProperty(PropertyResponse arg) => - new SonarQubeProperty(arg.Key, arg.Value); - - private sealed class PropertyResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs b/src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs deleted file mode 100644 index c78db01f77..0000000000 --- a/src/SonarQube.Client/Api/V5_20/GetQualityProfilesRequest.cs +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V5_20 -{ - public class GetQualityProfilesRequest : RequestBase, IGetQualityProfilesRequest - { - [JsonProperty("projectKey")] - public virtual string ProjectKey { get; set; } - - [JsonProperty("organization")] - public virtual string OrganizationKey { get; set; } - - [JsonProperty("defaults")] - public virtual bool? Defaults => string.IsNullOrWhiteSpace(ProjectKey) ? (bool?)true : null; - - protected override string Path => "api/qualityprofiles/search"; - - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - var result = await InvokeUncheckedAsync(httpClient, token); - - if (result.StatusCode == HttpStatusCode.NotFound) - { - Logger.Info("This project has no quality profile. Downloading the default quality profile."); - - // The project has not been scanned yet, get default quality profile - ProjectKey = null; - - result = await InvokeUncheckedAsync(httpClient, token); - } - - result.EnsureSuccess(); - - return result.Value; - } - - protected override SonarQubeQualityProfile[] ParseResponse(string response) => - JObject.Parse(response)["profiles"] - .ToObject() - .Select(FromResponse) - .ToArray(); - - private static SonarQubeQualityProfile FromResponse(QualityProfileResponse response) => - new SonarQubeQualityProfile( - response.Key, response.Name, response.Language, response.IsDefault, response.LastRuleChange); - - private sealed class QualityProfileResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("language")] - public string Language { get; set; } - - [JsonProperty("isDefault")] - public bool IsDefault { get; set; } - - [JsonProperty("rulesUpdatedAt")] - public DateTime LastRuleChange { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs b/src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs deleted file mode 100644 index 2b7bf9d928..0000000000 --- a/src/SonarQube.Client/Api/V5_50/GetRulesRequest.cs +++ /dev/null @@ -1,154 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V5_50 -{ - public class GetRulesRequest : PagedRequestBase, IGetRulesRequest - { - protected override string Path => "api/rules/search"; - - [JsonProperty("activation")] - public bool? IsActive { get; set; } - - [JsonProperty("qprofile")] - public string QualityProfileKey { get; set; } - - [JsonProperty("rule_key")] - public string RuleKey { get; set; } - - // Update this property if more fields are needed in the response. Have in mind - // that the field names here do not always correspond to the actual field names - // in the response! For example 'internalKey' in the request corresponds to 'key' - // in the response. The server error message (400) returns all supported fields. - // Also Make sure the field is supported in this version of the API. - // If not add a new request for API version that supports. e.g. "GetRulesWithDescriptionSectionsRequest" - - internal static readonly IList ResponseList = new List { "repo", "internalKey", "params", "actives" }; - - [JsonIgnore] - internal IList ResponseListField { get; set; } = ResponseList; - - [JsonProperty("f")] - public string ResponseFields => string.Join(",", ResponseListField); - - protected override SonarQubeRule[] ParseResponse(string response) - { - var responseJson = JObject.Parse(response); - - var activeQualityProfiles = ((JObject)responseJson["actives"]).Properties() - // Flatten the RuleKey-QualityProfile[] dictionary to make the lookup creation easier - .SelectMany( - p => p.Value - .ToObject() - .Select(q => new { Key = p.Name, QualityProfile = q })) - .ToLookup( - x => x.Key, - x => x.QualityProfile); - - return responseJson["rules"] - .ToObject() - .Select(rule => ToSonarQubeRule(rule, activeQualityProfiles[rule.Key])) - .ToArray(); - } - - private SonarQubeRule ToSonarQubeRule(RuleResponse response, - IEnumerable activeQualityProfiles) - { - var isActive = activeQualityProfiles.Any(); - - SonarQubeIssueSeverity severity; - Dictionary parameters; - if (isActive) - { - var activeQP = activeQualityProfiles.First(); - severity = SonarQubeIssueSeverityConverter.Convert(activeQP.Severity); - - // Optimisation: avoid creating objects if there are no parameters - parameters = activeQP.Parameters.Length > 0 ? - activeQP.Parameters.ToDictionary(p => p.Key, p => p.Value) : null; - } - else - { - severity = SonarQubeIssueSeverity.Unknown; - parameters = null; - } - - var issueType = SonarQubeIssueTypeConverter.Convert(response.Type); - - return new SonarQubeRule(GetRuleKey(response.Key), - response.RepositoryKey, - isActive, - severity, - CleanCodeTaxonomyHelpers.ToSonarQubeCleanCodeAttribute(response.CleanCodeAttribute), - CleanCodeTaxonomyHelpers.ToDefaultImpacts(response.Impacts), - parameters, - issueType); - } - - private static string GetRuleKey(string compositeKey) => - compositeKey.Substring(compositeKey.IndexOf(':') + 1); - - private sealed class RuleResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("repo")] - public string RepositoryKey { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("cleanCodeAttribute")] - public string CleanCodeAttribute { get; set; } - - [JsonProperty("impacts")] - public ServerImpact[] Impacts { get; set; } - } - - private sealed class QualityProfileResponse - { - [JsonProperty("qProfile")] - public string Key { get; set; } - - [JsonProperty("severity")] - public string Severity { get; set; } - - [JsonProperty("params")] - public ParameterResponse[] Parameters { get; set; } - } - - private sealed class ParameterResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs b/src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs deleted file mode 100644 index 66e7801dfa..0000000000 --- a/src/SonarQube.Client/Api/V6_30/GetPropertiesRequest.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V6_30 -{ - public class GetPropertiesRequest : RequestBase, IGetPropertiesRequest - { - [JsonProperty("component")] - public string ProjectKey { get; set; } - - protected override string Path => "api/settings/values"; - - protected override SonarQubeProperty[] ParseResponse(string response) => - JObject.Parse(response)["settings"] - .ToObject() - .SelectMany(ToProperties) - .ToArray(); - - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - var result = await InvokeUncheckedAsync(httpClient, token); - - if (result.StatusCode == HttpStatusCode.NotFound) - { - Logger.Info($"Project with key '{ProjectKey}' does not exist. Downloading the default properties."); - - ProjectKey = null; - - result = await InvokeUncheckedAsync(httpClient, token); - } - - result.EnsureSuccess(); - - return result.Value; - } - - private IEnumerable ToProperties(PropertyResponse arg) - { - if (arg.FieldValues != null) - { - for (int i = 0; i < arg.FieldValues.Length; i++) - { - var fieldValue = arg.FieldValues[i]; - foreach (var item in fieldValue) - { - yield return new SonarQubeProperty($"{arg.Key}.{i + 1}.{item.Key}", item.Value); - } - } - } - else if (arg.Values != null) - { - yield return new SonarQubeProperty(arg.Key, string.Join(",", arg.Values)); - } - else - { - yield return new SonarQubeProperty(arg.Key, arg.Value); - } - } - - private sealed class PropertyResponse - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - - [JsonProperty("values")] - public string[] Values { get; set; } - - [JsonProperty("fieldValues")] - public Dictionary[] FieldValues { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs b/src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs deleted file mode 100644 index 787bece22e..0000000000 --- a/src/SonarQube.Client/Api/V6_50/GetQualityProfilesRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; - -namespace SonarQube.Client.Api.V6_50 -{ - public class GetQualityProfilesRequest : V5_20.GetQualityProfilesRequest - { - [JsonProperty("project")] - public override string ProjectKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs b/src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs deleted file mode 100644 index 1e2d8fbd0d..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetExclusionsRequest.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V7_20 -{ - public class GetExclusionsRequest : RequestBase, IGetExclusionsRequest - { - internal const string ExclusionsKey = "sonar.exclusions"; - internal const string GlobalExclusionsKey = "sonar.global.exclusions"; - internal const string InclusionsKey = "sonar.inclusions"; - - [JsonProperty("component")] - public virtual string ProjectKey { get; set; } - - [JsonProperty("keys")] - public virtual string Keys { get; } = - string.Join(",", ExclusionsKey, GlobalExclusionsKey, InclusionsKey); - - protected override string Path => "api/settings/values"; - - protected override ServerExclusions ParseResponse(string response) - { - if (string.IsNullOrEmpty(response)) - { - return new ServerExclusions(); - } - - var jsonParse = JObject.Parse(response); - - var settings = jsonParse["settings"]?.ToObject(); - - if (settings?.Any() != true) - { - return new ServerExclusions(); - } - - var exclusions = GetValues(settings, ExclusionsKey); - var globalExclusions = GetValues(settings, GlobalExclusionsKey); - var inclusions = GetValues(settings, InclusionsKey); - - return new ServerExclusions( - exclusions: exclusions, - globalExclusions: globalExclusions, - inclusions: inclusions); - } - - private static string[] GetValues(IEnumerable settings, string key) - { - return settings.SingleOrDefault(x => x.Key == key)?.Values; - } - - private sealed class Setting - { - [JsonProperty("key")] - public string Key { get; set; } - - [JsonProperty("values")] - public string[] Values { get; set; } - } - } -} diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs deleted file mode 100644 index de060d3e66..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesRequest.cs +++ /dev/null @@ -1,186 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V7_20; - -/// -/// Generic get issues class. It does not support as it is server-type-specific. -/// See and for more details. -/// -internal class GetIssuesRequest : PagedRequestBase -{ - [JsonProperty("projects")] - public virtual string ProjectKey { get; set; } - - [JsonProperty("statuses")] - public string Statuses { get; set; } - - [JsonProperty("issues")] - public string IssueKeysAsString => IssueKeys == null ? null : string.Join(",", IssueKeys); - - [JsonIgnore] - public string[] IssueKeys { get; set; } - - //These are normally comma seperated values but we need only one for now. If we need to pass more than 1 value we'll need to change the structure similar to issueKeys - //For now it's not needed. - [JsonProperty("rules")] - public string RuleId { get; set; } - - // Notes: - // 1) Branch support is not available in SQ Community edition. SQ will just ignore it. - // 2) SonarQube has supported the parameter since v6.6. However, the LTS at the point - // we added added branch-awareness to SLVS was v8.9.10. To minimise the amount of - // work on the SLVS side, we'll add branch support from SQ v7.2. - [JsonProperty("branch", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue("")] - public string Branch { get; set; } - - // This property is not present in the IGetIssuesRequest interface, it is meant to be - // set by the GetIssuesRequestWrapper to add additional parameters to the API calls. - [JsonProperty("types")] - public string Types { get; set; } - - [JsonProperty("languages", DefaultValueHandling = DefaultValueHandling.Ignore), DefaultValue(null)] - public string Languages { get; set; } - - protected override string Path => "api/issues/search"; - - protected override SonarQubeIssue[] ParseResponse(string response) - { - var root = JObject.Parse(response); - - // This is a paged request so ParseResponse will be called once for each "page" - // of the response. However, we expect each page to be self-contained, so we want - // to rebuild the lookup each time. - componentKeyPathLookup = root.GetComponentKeyPathLookup(); - - return root["issues"] - .ToObject() - .Select(ToSonarQubeIssue) - .ToArray(); - } - - #region Json data classes -> public read-only class conversion methods - - /// - /// Lookup component key -> path for files. Each response contains normalized data, containing - /// issues and components, where each issue's "component" property points to a component with - /// the same "key". We obtain the FilePath of each issue from its corresponding component. - /// - private protected ILookup componentKeyPathLookup; - - private SonarQubeIssue ToSonarQubeIssue(ServerIssue issue) => - new SonarQubeIssue(issue.IssueKey, ComputePath(issue.Component), issue.Hash, issue.Message, ComputeModuleKey(issue), - issue.CompositeRuleKey, issue.Status == "RESOLVED", - SonarQubeIssueSeverityConverter.Convert(issue.Severity), - issue.CreationDate, - issue.UpdateDate, - issue.TextRange.ToIssueTextRange(), - ToIssueFlows(issue.Flows), - issue.ContextKey); - - private protected string ComputePath(string component) => - FilePathNormalizer.NormalizeSonarQubePath(componentKeyPathLookup[component].FirstOrDefault() ?? string.Empty); - - private protected static string ComputeModuleKey(ServerIssue issue) => - issue.SubProject ?? issue.Component; - - private protected List ToIssueFlows(ServerIssueFlow[] serverIssueFlows) => - serverIssueFlows?.Select(ToIssueFlow).ToList(); - - private protected IssueFlow ToIssueFlow(ServerIssueFlow serverIssueFlow) => - new IssueFlow(serverIssueFlow.Locations?.Select(ToIssueLocation).ToList()); - - private protected IssueLocation ToIssueLocation(ServerIssueLocation serverIssueLocation) => - new IssueLocation(ComputePath(serverIssueLocation.Component), serverIssueLocation.Component, serverIssueLocation.TextRange.ToIssueTextRange(), serverIssueLocation.Message); - - #endregion Json data classes -> public read-only class conversion methods - - #region JSON data classes - - private protected class ServerIssue - { - [JsonProperty("key")] - public string IssueKey { get; set; } - - [JsonProperty("rule")] - public string CompositeRuleKey { get; set; } - - [JsonProperty("component")] - public string Component { get; set; } - - [JsonProperty("subProject")] - public string SubProject { get; set; } - - [JsonProperty("hash")] - public string Hash { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("severity")] - public string Severity { get; set; } - - [JsonProperty("textRange")] - public ServerIssueTextRange TextRange { get; set; } - - [JsonProperty("creationDate")] - public virtual DateTimeOffset CreationDate { get; set; } - - [JsonProperty("updateDate")] - public virtual DateTimeOffset UpdateDate { get; set; } - - [JsonProperty("flows")] - public ServerIssueFlow[] Flows { get; set; } - - [JsonProperty("ruleDescriptionContextKey")] - public string ContextKey { get; set; } - } - - private protected sealed class ServerIssueFlow - { - [JsonProperty("locations")] - public ServerIssueLocation[] Locations { get; set; } - } - - private protected sealed class ServerIssueLocation - { - [JsonProperty("component")] - public string Component { get; set; } - - [JsonProperty("textRange")] - public ServerIssueTextRange TextRange { get; set; } - - [JsonProperty("msg")] - public string Message { get; set; } - } - - #endregion // JSON data classes -} diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs deleted file mode 100644 index fdb3540375..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesRequestWrapper.cs +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net.Http; -using SonarQube.Client.Logging; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Api.V7_20; - -/// -/// The SonarQube 10k API result limit problem (https://github.com/SonarSource/sonarlint-visualstudio/issues/776): -/// SonarQube will return the first 10k results from any query.The suppressed issues in large -/// projects could be more than 10k and SLVS will not hide those which are not returned by the -/// server. -/// -/// To reduce the effects of this limitation we will retrieve issues in batches by issue type. -/// The same approach is used in the other flavours of SonarLint. -/// -/// This class should be removed if/when SonarQube removes the 10k API result limitation. -/// -internal class GetIssuesRequestWrapper : IGetIssuesRequest - where T : GetIssuesWithComponentRequest, new() -{ - private readonly T innerRequest = new T(); - - public string ProjectKey { get; set; } - - public string Statuses { get; set; } - - public string Branch { get; set; } - - public string[] IssueKeys { get; set; } - - public string RuleId { get; set; } - - public string ComponentKey { get; set; } - public string Languages { get; set; } - public ILogger Logger { get; set; } - - public async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - // Transfer all IGetIssuesRequest properties to the inner request. If more properties are - // added to IGetIssuesRequest, this block should set them. - innerRequest.ProjectKey = ProjectKey; - innerRequest.Statuses = Statuses; - innerRequest.Branch = Branch; - innerRequest.Logger = Logger; - innerRequest.IssueKeys = IssueKeys; - innerRequest.RuleId = RuleId; - innerRequest.ComponentKey = ComponentKey; - innerRequest.Languages = Languages; - - if (innerRequest.IssueKeys != null) - { - var response = await innerRequest.InvokeAsync(httpClient, token); - - return response; - } - - ResetInnerRequest(); - innerRequest.Types = "CODE_SMELL"; - var codeSmells = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(codeSmells, innerRequest, "code smells"); - - ResetInnerRequest(); - innerRequest.Types = "BUG"; - var bugs = await innerRequest.InvokeAsync(httpClient, token); - WarnForApiLimit(bugs, innerRequest, "bugs"); - return codeSmells - .Concat(bugs) - .ToArray(); - } - - private void WarnForApiLimit(SonarQubeIssue[] issues, GetIssuesRequest request, string friendlyIssueType) - { - if (issues.Length == request.ItemsLimit) - { - Logger.Warning($"Sonar web API response limit reached ({request.ItemsLimit} items). Some {friendlyIssueType} might not be suppressed."); - } - } - - /// - /// For paged requests the Page property is automatically changed on each invocation. - /// We are resetting it so that our invocations for different issue types could start - /// from the first page. - /// - private void ResetInnerRequest() - { - innerRequest.Page = 1; - } -} diff --git a/src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs b/src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs deleted file mode 100644 index 04e62c461d..0000000000 --- a/src/SonarQube.Client/Api/V7_20/GetIssuesWithComponentRequest.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; - -namespace SonarQube.Client.Api.V7_20 -{ - // This class should not be used on it's own - internal abstract class GetIssuesWithComponentRequest : GetIssuesRequest - { - public abstract string ComponentKey { get; set; } - } - - /// - /// This class is used to override the query string property name for for - /// - /// - /// Reason to create this class https://github.com/SonarSource/sonarlint-visualstudio/issues/5181 - /// - internal class GetIssuesWithComponentSonarCloudRequest : GetIssuesWithComponentRequest, IGetIssuesRequest - { - [JsonProperty("componentKeys")] - public override string ComponentKey { get; set; } - } - - /// - /// This class is used to override the query string property name for for - /// - /// - /// Reason to create this class https://github.com/SonarSource/sonarlint-visualstudio/issues/5181 - /// - internal class GetIssuesWithComponentSonarQubeRequest : GetIssuesWithComponentRequest, IGetIssuesRequest - { - [JsonProperty("components")] - public override string ComponentKey { get; set; } - } -} diff --git a/src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs b/src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs deleted file mode 100644 index 1400e969f5..0000000000 --- a/src/SonarQube.Client/Api/V9_4/GetSonarLintEventStream.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Net.Http; -using System.Net.Http.Headers; -using Newtonsoft.Json; -using SonarLint.VisualStudio.Core; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V9_4; - -internal class GetSonarLintEventStream : RequestBase, IGetSonarLintEventStream -{ - private static readonly string RoslynServerLanguageKeys = string.Join(",", LanguageProvider.Instance.RoslynLanguages.Select(x => x.ServerLanguageKey)); - - protected override string Path => "api/push/sonarlint_events"; - - protected override MediaTypeWithQualityHeaderValue[] AllowedMediaTypeHeaders => new[] { MediaTypeWithQualityHeaderValue.Parse("text/event-stream") }; - - protected override async Task> ReadResponseAsync(HttpResponseMessage httpResponse) - { - var stream = await httpResponse.Content.ReadAsStreamAsync(); - - return new Result(httpResponse, stream); - } - - protected override Stream ParseResponse(string response) - { - // should not be called - throw new InvalidOperationException(); - } - - /// - /// Supports only Roslyn languages as the SSE for non-Roslyn languages are handled by SLCore. - /// - [JsonProperty("languages")] - public string Languages { get; set; } = RoslynServerLanguageKeys; - - [JsonProperty("projectKeys")] - public string ProjectKey { get; set; } -} diff --git a/src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs b/src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs deleted file mode 100644 index 07ef2534cf..0000000000 --- a/src/SonarQube.Client/Api/V9_6/GetIssuesWithContextRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Newtonsoft.Json; -using SonarQube.Client.Api.V7_20; - -namespace SonarQube.Client.Api.V9_6 -{ - /// - /// This class does not support component-based search. See for more information - /// - internal class GetIssuesWithContextRequest : GetIssuesRequest - { - [JsonProperty("additionalFields")] - public string AdditionalFields => "ruleDescriptionContextKey"; - } -} diff --git a/src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs b/src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs deleted file mode 100644 index 62115af24a..0000000000 --- a/src/SonarQube.Client/Api/V9_9/SearchFilesByNameRequest.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Helpers; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V9_9 -{ - internal class SearchFilesByNameRequest : PagedRequestBase, ISearchFilesByNameRequest - { - protected override string Path => "api/components/tree"; - - [JsonProperty("component")] - public string ProjectKey { get; set; } - - [JsonProperty("branch")] - public string BranchName { get; set; } - - [JsonProperty("q")] - public string FileName { get; set; } - - //File and Test Files - [JsonProperty("qualifiers")] - public string Qualifiers => "FIL,UTS"; - - protected override string[] ParseResponse(string response) - { - var root = JObject.Parse(response); - - return root.GetComponentPathList().Select(FilePathNormalizer.NormalizeSonarQubePath).ToArray(); - } - } -} diff --git a/src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs b/src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs deleted file mode 100644 index 790c522c68..0000000000 --- a/src/SonarQube.Client/Helpers/CleanCodeTaxonomyHelpers.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using SonarQube.Client.Api.Common; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Helpers -{ - internal static class CleanCodeTaxonomyHelpers - { - internal static SonarQubeCleanCodeAttribute? ToSonarQubeCleanCodeAttribute(string value) - { - if (value == null) - { - return null; - } - - return (SonarQubeCleanCodeAttribute)Enum.Parse(typeof(SonarQubeCleanCodeAttribute), value, true); - } - - internal static Dictionary ToDefaultImpacts(ServerImpact[] impacts) - { - return impacts? - .ToDictionary(i => (SonarQubeSoftwareQuality)Enum.Parse(typeof(SonarQubeSoftwareQuality), i.SoftwareQuality, true), - i => (SonarQubeSoftwareQualitySeverity)Enum.Parse(typeof(SonarQubeSoftwareQualitySeverity), i.Severity, true)); - } - } -} diff --git a/src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs b/src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs deleted file mode 100644 index dc7b83ab24..0000000000 --- a/src/SonarQube.Client/Helpers/ComponentKeyGenerator.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO; - -namespace SonarQube.Client.Helpers -{ - public static class ComponentKeyGenerator - { - /// - /// Generates Sonar server component key - /// - /// Path for the local file. - /// Path for the local root of the project. Server paths are relative to this. - /// Sonar server project key - /// Component key in the format projectkey:relativepath - /// Invalid root format or the local path is not under the root. - public static string GetComponentKey(string localFilePath, string projectRootPath, string projectKey) - { - if (!Path.IsPathRooted(projectRootPath) || !projectRootPath.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - throw new ArgumentException("Invalid root path format"); - } - - if (!localFilePath.StartsWith(projectRootPath)) - { - throw new ArgumentException("Local path is not under this root"); - } - - var serverFilePath = FilePathNormalizer.ServerizeWindowsPath(localFilePath.Substring(projectRootPath.Length)); - return $"{projectKey}:{serverFilePath}"; - } - } -} diff --git a/src/SonarQube.Client/ISonarQubeService.cs b/src/SonarQube.Client/ISonarQubeService.cs index d74519288d..20e026b2f6 100644 --- a/src/SonarQube.Client/ISonarQubeService.cs +++ b/src/SonarQube.Client/ISonarQubeService.cs @@ -19,7 +19,6 @@ */ using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents; namespace SonarQube.Client; @@ -41,22 +40,6 @@ Task> GetNotificationEventsAsync( DateTimeOffset eventsSince, CancellationToken token); - /// - /// Returns the list of server paths with matching file names - /// - /// The project identifier - /// - /// (optional) The Sonar branch for which issues should be returned. If null/empty, - /// the issues for the "main" branch will be returned - /// - /// The file name used for the search - /// - Task> SearchFilesByNameAsync( - string projectKey, - string branch, - string fileName, - CancellationToken token); - /// /// Returns the URI to view the specified issue on the server /// @@ -67,9 +50,4 @@ Task> SearchFilesByNameAsync( /// Returns branch information for the specified project key /// Task> GetProjectBranchesAsync(string projectKey, CancellationToken token); - - /// - /// Creates a new for the given - /// - Task CreateSSEStreamReader(string projectKey, CancellationToken token); } diff --git a/src/SonarQube.Client/Models/ServerExclusions.cs b/src/SonarQube.Client/Models/ServerExclusions.cs deleted file mode 100644 index 12627ad7ae..0000000000 --- a/src/SonarQube.Client/Models/ServerExclusions.cs +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using SonarLint.VisualStudio.Core.CSharpVB; - -namespace SonarQube.Client.Models -{ - public sealed class ServerExclusions : IEquatable, IFileExclusions - { - private static readonly string[] EmptyValues = Array.Empty(); - - public ServerExclusions() - : this(null, null, null) - { - } - - public ServerExclusions(IEnumerable exclusions, - IEnumerable globalExclusions, - IEnumerable inclusions) - { - Exclusions = AddPathPrefixIfNeeded(exclusions) ?? EmptyValues; - GlobalExclusions = AddPathPrefixIfNeeded(globalExclusions) ?? EmptyValues; - Inclusions = AddPathPrefixIfNeeded(inclusions) ?? EmptyValues; - } - - /// - /// Similarly to the other SL flavors, we will prefix everything with "**/" it's not already prefixed. - /// - private static string[] AddPathPrefixIfNeeded(IEnumerable paths) => - paths?.Select(path => path.StartsWith("**/") ? path : $"**/{path}").ToArray(); - - [JsonProperty("sonar.exclusions")] - public string[] Exclusions { get; set; } - - [JsonProperty("sonar.global.exclusions")] - public string[] GlobalExclusions { get; set; } - - [JsonProperty("sonar.inclusions")] - public string[] Inclusions { get; set; } - - public override string ToString() - { - return "Server Exclusions: " + - "\n Inclusions: " + string.Join(",", Inclusions) + - "\n Exclusions: " + string.Join(",", Exclusions) + - "\n Global Exclusions: " + string.Join(",", GlobalExclusions); - } - - public override bool Equals(object obj) - { - return Equals(obj as ServerExclusions); - } - - public bool Equals(ServerExclusions other) - { - if (other == null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return ToString().Equals(other.ToString()); - } - - public override int GetHashCode() - { - return ToString().GetHashCode(); - } - - public Dictionary ToDictionary() - { - var exclusions = new Dictionary - { - {"sonar.exclusions", string.Join(",", Exclusions)}, - {"sonar.global.exclusions", string.Join(",", GlobalExclusions)}, - {"sonar.inclusions", string.Join(",", Inclusions)} - }; - - return exclusions; - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs deleted file mode 100644 index b555b0303d..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IIssueChangedServerEvent.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - - -using System; -using System.Text; -using Newtonsoft.Json; - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - /// - /// Represents IssueChanged server event information - /// - public interface IIssueChangedServerEvent : IServerEvent - { - string ProjectKey { get; } - bool IsResolved { get; } - IBranchAndIssueKey[] BranchAndIssueKeys { get; } - - // also has Severity and Type that we don't care about - } - - public class IssueChangedServerEvent : IIssueChangedServerEvent - { - public IssueChangedServerEvent(string projectKey, bool isResolved, BranchAndIssueKey[] issues) - { - ProjectKey = projectKey ?? throw new ArgumentNullException(nameof(projectKey)); - IsResolved = isResolved; - BranchAndIssueKeys = issues ?? throw new ArgumentNullException(nameof(issues)); - } - - [JsonProperty("projectKey")] - public string ProjectKey { get; } - - [JsonProperty("resolved")] - public bool IsResolved { get; } - - [JsonProperty("issues")] - public IBranchAndIssueKey[] BranchAndIssueKeys { get; } - - // Display-friendly override for logging/debugging - public override string ToString() - { - var sb = new StringBuilder(); - sb.AppendLine($"ProjectKey: {ProjectKey}, IsResolved: {IsResolved}, Issue count: " + BranchAndIssueKeys.Length); - foreach (var item in BranchAndIssueKeys) - { - sb.AppendLine($"\tBranch: {item.BranchName}, IssueKey: {item.IssueKey}"); - } - - return sb.ToString(); - } - } - - /// - /// Tuple of the changed issue key in a specific branch - /// - public interface IBranchAndIssueKey - { - string BranchName { get; } - string IssueKey { get; } - } - - public class BranchAndIssueKey : IBranchAndIssueKey - { - public BranchAndIssueKey(string issueKey, string branchName) - { - IssueKey = issueKey ?? throw new ArgumentNullException(nameof(issueKey)); - BranchName = branchName ?? throw new ArgumentNullException(nameof(branchName)); - } - - [JsonProperty("branchName")] - public string BranchName { get; } - - [JsonProperty("issueKey")] - public string IssueKey { get; } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs deleted file mode 100644 index a72484f34b..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IQualityProfileEvent.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - /// - /// Represents RuleSetChanged server event - /// - public interface IQualityProfileEvent : IServerEvent - { - // empty, as we decided to ignore the event payload & treat this as an indicator/trigger only - } - - public class QualityProfileEvent : IQualityProfileEvent - { - // empty, as we decided to ignore the event payload & treat this as an indicator/trigger only - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs deleted file mode 100644 index d43c9744e3..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ClientContract/IServerEvent.cs +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models.ServerSentEvents.ClientContract -{ - public interface IServerEvent - { - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs b/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs deleted file mode 100644 index 32e74cda5b..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReader.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; -using SonarQube.Client.Models.ServerSentEvents.ClientContract; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; -using System.Collections.Generic; -using SonarQube.Client.Logging; - -namespace SonarQube.Client.Models.ServerSentEvents -{ - public interface ISSEStreamReader - { - /// - /// Wraps the stream response from the server, reads from it and converts it to . - /// Will block the calling thread until an event exists or the connection is closed. - /// - /// - /// Can return null (i.e. if the underlying event type is unsupported). - /// - Task ReadAsync(); - } - - /// - /// Returns deserialized from - /// Code on the java side: https://github.com/SonarSource/sonarlint-core/blob/4f34c7c844b12e331a61c63ad7105acac41d2efd/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/PushApi.java - /// - internal class SSEStreamReader : ISSEStreamReader - { - private readonly ISqSSEStreamReader sqSSEStreamReader; - private readonly ILogger logger; - - private readonly IDictionary eventTypeToDataTypeMap = new Dictionary - { - {"IssueChanged", typeof(IssueChangedServerEvent)}, - {"RuleSetChanged", typeof(QualityProfileEvent)}, - }; - - public SSEStreamReader(ISqSSEStreamReader sqSSEStreamReader, ILogger logger) - { - this.sqSSEStreamReader = sqSSEStreamReader; - this.logger = logger; - } - - public async Task ReadAsync() - { - var sqEvent = await ReadNextEventAsync(); - - if (sqEvent == null || !eventTypeToDataTypeMap.ContainsKey(sqEvent.Type)) - { - return null; - } - - try - { - var deserializedEvent = JsonConvert.DeserializeObject(sqEvent.Data, eventTypeToDataTypeMap[sqEvent.Type]); - - return (IServerEvent) deserializedEvent; - } - catch (Exception ex) - { - logger.Debug("[SSEStreamReader] Failed to deserialize sq event." + - $"\n Exception: {ex}" + - $"\n Raw event type: {sqEvent.Type}" + - $"\n Raw event data: {sqEvent.Data}"); - - return null; - } - } - - private async Task ReadNextEventAsync() - { - try - { - return await sqSSEStreamReader.ReadAsync(); - } - catch (Exception) - { - sqSSEStreamReader.Dispose(); - throw; - } - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs b/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs deleted file mode 100644 index 20b1d5e57f..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/SSEStreamReaderFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using System.Threading; -using SonarQube.Client.Logging; -using SonarQube.Client.Models.ServerSentEvents.ServerContract; - -namespace SonarQube.Client.Models.ServerSentEvents -{ - internal interface ISSEStreamReaderFactory - { - ISSEStreamReader Create(Stream networkStream, CancellationToken cancellationToken); - } - - internal class SSEStreamReaderFactory : ISSEStreamReaderFactory - { - private readonly ILogger logger; - - public SSEStreamReaderFactory(ILogger logger) - { - this.logger = logger; - } - - public ISSEStreamReader Create(Stream networkStream, CancellationToken cancellationToken) - { - var sqStreamReader = new SqSSEStreamReader(new StreamReader(networkStream), cancellationToken); - var sseStreamReader = new SSEStreamReader(sqStreamReader, logger); - - return sseStreamReader; - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs b/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs deleted file mode 100644 index 3599e2d69f..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerEvent.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models.ServerSentEvents.ServerContract -{ - /// - /// Represents the raw event information coming from the server - /// - internal interface ISqServerEvent - { - string Type { get; } - - /// - /// Json-serialized event data - /// - string Data { get; } - } - - internal class SqServerEvent : ISqServerEvent - { - public SqServerEvent(string type, string data) - { - Type = type; - Data = data; - } - - public string Type { get; } - public string Data { get; } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs b/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs deleted file mode 100644 index c05429b84a..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/ISqServerSentEventParser.cs +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Linq; - -namespace SonarQube.Client.Models.ServerSentEvents.ServerContract -{ - internal interface ISqServerSentEventParser - { - ISqServerEvent Parse(IReadOnlyList eventLines); - } - - /// - /// Parses the given string into - /// - /// - /// Parser code on the java side: https://github.com/SonarSource/sonarlint-core/blob/4f34c7c844b12e331a61c63ad7105acac41d2efd/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventParser.java#L24\ - /// - internal class SqServerSentEventParser : ISqServerSentEventParser - { - private const string EventTypeFieldPrefix = "event: "; - private const string DataFieldPrefix = "data: "; - - public ISqServerEvent Parse(IReadOnlyList eventLines) - { - if (eventLines == null || !eventLines.Any()) - { - return null; - } - - var eventType = ParseEventType(eventLines); - var eventData = ParseEventData(eventLines); - - if (string.IsNullOrEmpty(eventType) || string.IsNullOrEmpty(eventData)) - { - return null; - } - - return new SqServerEvent(eventType, eventData); - } - - private string ParseEventType(IEnumerable eventLines) - { - var eventType = eventLines - .FirstOrDefault(x => x.StartsWith(EventTypeFieldPrefix)) - ?.Substring(EventTypeFieldPrefix.Length); - - return eventType; - } - - private string ParseEventData(IEnumerable eventLines) - { - var validDataEventLines = eventLines - .Where(x => x.StartsWith(DataFieldPrefix)) - .Select(x => x.Substring(DataFieldPrefix.Length)); - - var validEventData = string.Join("", validDataEventLines); - - return validEventData; - } - } -} diff --git a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs b/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs deleted file mode 100644 index 8682bca666..0000000000 --- a/src/SonarQube.Client/Models/ServerSentEvents/ServerContract/SqSSEStreamReader.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SonarQube.Client.Models.ServerSentEvents.ServerContract -{ - /// - /// Reads lines from the network stream and aggregates them into . - /// - /// - /// Returns aggregated or null if the stream ended or the task was cancelled. - /// Will throw if there was a problem reading from the underlying stream. - /// - internal interface ISqSSEStreamReader : IDisposable - { - Task ReadAsync(); - } - - /// - /// Code on the java side: https://github.com/SonarSource/sonarlint-core/blob/171ca4d75c24033e115a81bd7481427cd1f39f4c/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventBuffer.java - /// - internal sealed class SqSSEStreamReader : ISqSSEStreamReader - { - private readonly StreamReader networkStreamReader; - private readonly CancellationToken cancellationToken; - private readonly ISqServerSentEventParser sqServerSentEventParser; - - public SqSSEStreamReader(StreamReader networkStreamReader, CancellationToken cancellationToken) - : this(networkStreamReader, cancellationToken, new SqServerSentEventParser()) - { - } - - internal SqSSEStreamReader(StreamReader networkStreamReader, - CancellationToken cancellationToken, - ISqServerSentEventParser sqServerSentEventParser) - { - this.networkStreamReader = networkStreamReader; - this.cancellationToken = cancellationToken; - this.sqServerSentEventParser = sqServerSentEventParser; - } - - public async Task ReadAsync() - { - var eventLines = new List(); - - while (!networkStreamReader.EndOfStream && !cancellationToken.IsCancellationRequested) - { - var line = await networkStreamReader.ReadLineAsync(); - var isEventEnd = string.IsNullOrEmpty(line); - - if (isEventEnd) - { - var parsedEvent = sqServerSentEventParser.Parse(eventLines); - - eventLines.Clear(); - - if (parsedEvent != null) - { - return parsedEvent; - } - } - else - { - eventLines.Add(line); - } - } - - return null; - } - - public void Dispose() - { - networkStreamReader.Dispose(); - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs b/src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs deleted file mode 100644 index 890137f6fa..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeCleanCodeTaxonomy.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models -{ - public enum SonarQubeCleanCodeAttribute - { - // Consistency - Conventional, - Formatted, - Identifiable, - - // Intentionality - Clear, - Complete, - Efficient, - Logical, - - // Adaptability - Distinct, - Focused, - Modular, - Tested, - - // Responsibility - Lawful, - Respectful, - Trustworthy - } - - public enum SonarQubeSoftwareQuality - { - Maintainability, - Reliability, - Security - } - - public enum SonarQubeSoftwareQualitySeverity - { - Info = 0, - Low = 1, - Medium = 2, - High = 3, - Blocker = 4 - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeIssue.cs b/src/SonarQube.Client/Models/SonarQubeIssue.cs deleted file mode 100644 index bfba33340d..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeIssue.cs +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Collections.Generic; - -namespace SonarQube.Client.Models -{ - public class SonarQubeIssue - { - private static readonly IReadOnlyList EmptyFlows = new List().AsReadOnly(); - - public SonarQubeIssue(string issueKey, string filePath, string hash, string message, string moduleKey, string ruleId, bool isResolved, - SonarQubeIssueSeverity severity, DateTimeOffset creationTimestamp, DateTimeOffset lastUpdateTimestamp, - IssueTextRange textRange, List flows, string context = null, SonarQubeCleanCodeAttribute? cleanCodeAttribute = null, - Dictionary defaultImpacts = null) - { - IssueKey = issueKey; - FilePath = filePath; - Hash = hash; - Message = message; - ModuleKey = moduleKey; - RuleId = ruleId; - IsResolved = isResolved; - Severity = severity; - CreationTimestamp = creationTimestamp; - LastUpdateTimestamp = lastUpdateTimestamp; - TextRange = textRange; - Flows = flows ?? EmptyFlows; - Context = context; - CleanCodeAttribute = cleanCodeAttribute; - DefaultImpacts = defaultImpacts; - } - - public string IssueKey { get; } - - /// - /// Relative file path - /// - /// - /// The path is relative to the Sonar project root. - /// The path is in Windows format i.e. the directory separators are backslashes - /// - public string FilePath { get; } - - public string Hash { get; } - public string Message { get; } - public string ModuleKey { get; } - public string RuleId { get; } - - /// - /// This needs to be mutable as SLVS will update it during runtime. - /// - public bool IsResolved { get; set; } - - public SonarQubeIssueSeverity Severity { get; } - public IssueTextRange TextRange { get; } - public DateTimeOffset CreationTimestamp { get; } - public DateTimeOffset LastUpdateTimestamp { get; } - public IReadOnlyList Flows { get; } - - public string Context { get; set; } - - public SonarQubeCleanCodeAttribute? CleanCodeAttribute { get; } - - public Dictionary DefaultImpacts { get; set; } - } - - public class IssueFlow - { - private static readonly IReadOnlyList EmptyLocations = new List().AsReadOnly(); - - public IssueFlow(List locations) - { - Locations = locations ?? EmptyLocations; - } - - public IReadOnlyList Locations { get; } - } - - public class IssueLocation - { - public IssueLocation(string filePath, string moduleKey, IssueTextRange textRange, string message) - { - FilePath = filePath; - ModuleKey = moduleKey; - TextRange = textRange; - Message = message; - } - - public string FilePath { get; } - public string ModuleKey { get; } - public IssueTextRange TextRange { get; } - public string Message { get; } - - /// - /// Note: currently the hash for secondary locations is calculated - /// post-construction, so we need to be able to update this property - /// - public string Hash { get; internal set; } - } - - public class IssueTextRange - { - public IssueTextRange(int startLine, int endLine, int startOffset, int endOffset) - { - StartLine = startLine; - EndLine = endLine; - StartOffset = startOffset; - EndOffset = endOffset; - } - - public int StartLine { get; } - public int EndLine { get; } - public int StartOffset { get; } - public int EndOffset { get; } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs b/src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs deleted file mode 100644 index 02864df683..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeIssueSeverity.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; - -namespace SonarQube.Client.Models -{ - public enum SonarQubeIssueSeverity - { - Unknown = 0, - Info = 1, - Minor = 2, - Major = 3, - Critical = 4, - Blocker = 5 - } - - internal static class SonarQubeIssueSeverityConverter - { - public static SonarQubeIssueSeverity Convert(string data) - { - SonarQubeIssueSeverity severity; - if (!Enum.TryParse(data, true /* ignore case */, out severity)) - { - return SonarQubeIssueSeverity.Unknown; - } - return severity; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeIssueType.cs b/src/SonarQube.Client/Models/SonarQubeIssueType.cs deleted file mode 100644 index 6ebc0ff5dc..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeIssueType.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models -{ - public enum SonarQubeIssueType - { - Unknown = 0, - CodeSmell = 1, - Bug = 2, - Vulnerability = 3, - SecurityHotspot = 4 - } - - internal static class SonarQubeIssueTypeConverter - { - public static SonarQubeIssueType Convert(string data) - { - switch (data?.ToUpperInvariant()) - { - case "CODE_SMELL": - return SonarQubeIssueType.CodeSmell; - case "BUG": - return SonarQubeIssueType.Bug; - case "SECURITY_HOTSPOT": - return SonarQubeIssueType.SecurityHotspot; - case "VULNERABILITY": - return SonarQubeIssueType.Vulnerability; - default: - return SonarQubeIssueType.Unknown; - } - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeProject.cs b/src/SonarQube.Client/Models/SonarQubeProject.cs deleted file mode 100644 index db256beb44..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeProject.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; - -namespace SonarQube.Client.Models -{ - public class SonarQubeProject - { - // Ordinal comparer should be good enough: http://docs.sonarqube.org/display/SONAR/Project+Administration#ProjectAdministration-AddingaProject - public static readonly StringComparer KeyComparer = StringComparer.Ordinal; - - public string Key { get; } - public string Name { get; } - - public SonarQubeProject(string key, string name) - { - Key = key; - Name = name; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeProperty.cs b/src/SonarQube.Client/Models/SonarQubeProperty.cs deleted file mode 100644 index 501aae1c32..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeProperty.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Models -{ - public class SonarQubeProperty - { - public const string TestProjectRegexKey = "sonar.cs.msbuild.testProjectPattern"; - public const string TestProjectRegexDefaultValue = @"[^\\]*test[^\\]*$"; - - public string Key { get; } - public string Value { get; } - - public SonarQubeProperty(string key, string value) - { - Key = key; - Value = value; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeQualityProfile.cs b/src/SonarQube.Client/Models/SonarQubeQualityProfile.cs deleted file mode 100644 index a6aedc9bc4..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeQualityProfile.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; - -namespace SonarQube.Client.Models -{ - public class SonarQubeQualityProfile - { - // Ordinal comparer, similar to project key comparer - public static readonly StringComparer KeyComparer = StringComparer.Ordinal; - - public string Key { get; } - public string Name { get; } - public string Language { get; } - public bool IsDefault { get; } - public DateTime TimeStamp { get; } - - public SonarQubeQualityProfile(string key, string name, string language, bool isDefault, DateTime timeStamp) - { - Key = key; - Name = name; - Language = language; - IsDefault = isDefault; - TimeStamp = timeStamp; - } - } -} diff --git a/src/SonarQube.Client/Models/SonarQubeRule.cs b/src/SonarQube.Client/Models/SonarQubeRule.cs deleted file mode 100644 index c86108c41d..0000000000 --- a/src/SonarQube.Client/Models/SonarQubeRule.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Collections.ObjectModel; -using SonarLint.VisualStudio.Core.CSharpVB; - -namespace SonarQube.Client.Models -{ - public class SonarQubeRule : IRuleParameters - { - /// - /// Singleton to prevent creating unnecessary objects - /// - private static readonly IReadOnlyDictionary Empty = new ReadOnlyDictionary(new Dictionary()); - - public SonarQubeRule(string key, - string repositoryKey, - bool isActive, - SonarQubeIssueSeverity severity, - SonarQubeCleanCodeAttribute? cleanCodeAttribute, - Dictionary softwareQualitySeverities, - IDictionary parameters, - SonarQubeIssueType issueType) - { - Key = key; - RepositoryKey = repositoryKey; - IsActive = isActive; - Severity = severity; - CleanCodeAttribute = cleanCodeAttribute; - SoftwareQualitySeverities = softwareQualitySeverities; - IssueType = issueType; - - if (parameters == null || parameters.Count == 0) - { - Parameters = Empty; - } - else - { - Parameters = new ReadOnlyDictionary(parameters); - } - } - - public string Key { get; } - - public string RepositoryKey { get; } - - public bool IsActive { get; } - - public SonarQubeIssueSeverity Severity { get; } - - public SonarQubeCleanCodeAttribute? CleanCodeAttribute { get; } - - public Dictionary SoftwareQualitySeverities { get; } - - /// - /// When the rule is active, contains the parameters that are set in the corresponding quality profile. - /// This is empty dictionary if the rule is inactive, or does not have parameters. - /// - public IReadOnlyDictionary Parameters { get; } - - public SonarQubeIssueType IssueType { get; } - } -} diff --git a/src/SonarQube.Client/Requests/IPagedRequest.cs b/src/SonarQube.Client/Requests/IPagedRequest.cs deleted file mode 100644 index abcd18f2a3..0000000000 --- a/src/SonarQube.Client/Requests/IPagedRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarQube.Client.Requests -{ - /// - /// Implement this interface for requests that return multiple items in pages. - /// - /// The type of the items returned by this request. - public interface IPagedRequest : IRequest - { - int Page { get; set; } - - int PageSize { get; set; } - int ItemsLimit { get; set; } - } -} diff --git a/src/SonarQube.Client/Requests/PagedRequestBase.cs b/src/SonarQube.Client/Requests/PagedRequestBase.cs deleted file mode 100644 index 7a9c3601dc..0000000000 --- a/src/SonarQube.Client/Requests/PagedRequestBase.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace SonarQube.Client.Requests -{ - /// - /// Abstract implementation of IPagedRequest that automatically downloads all pages - /// from the server. - /// - /// The type of the items returned by this request. - public abstract class PagedRequestBase : RequestBase, IPagedRequest - { - private const int FirstPage = 1; - public const int MaximumPageSize = 500; - public const int MaximumItemsCount = 10000; - - [JsonIgnore] public virtual int ItemsLimit { get; set; } = MaximumItemsCount; - - [JsonProperty("p")] - public virtual int Page { get; set; } = FirstPage; - - [JsonProperty("ps")] - public virtual int PageSize { get; set; } = MaximumPageSize; - - public override async Task InvokeAsync(HttpClient httpClient, CancellationToken token) - { - Debug.Assert(ItemsLimit > 0 && ItemsLimit <= MaximumItemsCount, - $"Invalid ItemsLimit: {ItemsLimit}. Expecting a value in the range [1, {MaximumItemsCount}]"); - - var allResponseItems = new List(); - - Result pageResult; - do - { - pageResult = await InvokeUncheckedAsync(httpClient, token); - ValidateResult(pageResult, allResponseItems); - - if (pageResult.Value != null) - { - allResponseItems.AddRange(pageResult.Value); - Logger.Debug($"Received {pageResult.Value.Length} items."); - } - - Page++; - } - while (allResponseItems.Count < ItemsLimit && - pageResult.Value != null && - pageResult.Value.Length >= PageSize); - - if (allResponseItems.Count < ItemsLimit) - { - return allResponseItems.ToArray(); - } - - if (allResponseItems.Count == MaximumItemsCount) - { - Logger.Warning("Sonar web maximum API response limit reached."); - } - - return allResponseItems.Take(ItemsLimit).ToArray(); - } - - protected virtual void ValidateResult(Result pageResult, List allResponseItems) => - pageResult.EnsureSuccess(); - } -} diff --git a/src/SonarQube.Client/SonarQubeService.cs b/src/SonarQube.Client/SonarQubeService.cs index 2c34ac2038..4c462b661b 100644 --- a/src/SonarQube.Client/SonarQubeService.cs +++ b/src/SonarQube.Client/SonarQubeService.cs @@ -18,14 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; using System.Net.Http; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; using SonarQube.Client.Api; using SonarQube.Client.Helpers; using SonarQube.Client.Models; -using SonarQube.Client.Models.ServerSentEvents; using SonarQube.Client.Requests; using ILogger = SonarQube.Client.Logging.ILogger; @@ -36,16 +34,14 @@ public class SonarQubeService : ISonarQubeService, IDisposable private const string MinSqVersionSupportingBearer = "10.4"; private readonly IHttpClientHandlerFactory httpClientHandlerFactory; private readonly ILogger logger; - private readonly ILanguageProvider languageProvider; private readonly IRequestFactorySelector requestFactorySelector; - private readonly ISSEStreamReaderFactory sseStreamReaderFactory; private readonly string userAgent; private HttpClient currentHttpClient; private ServerInfo currentServerInfo; private IRequestFactory requestFactory; public SonarQubeService(string userAgent, ILogger logger, ILanguageProvider languageProvider) - : this(new HttpClientHandlerFactory(new ProxyDetector(), logger), userAgent, logger, languageProvider, new RequestFactorySelector(), new SSEStreamReaderFactory(logger)) + : this(new HttpClientHandlerFactory(new ProxyDetector(), logger), userAgent, logger, new RequestFactorySelector()) { } @@ -53,17 +49,13 @@ public SonarQubeService(string userAgent, ILogger logger, ILanguageProvider lang IHttpClientHandlerFactory httpClientHandlerFactory, string userAgent, ILogger logger, - ILanguageProvider languageProvider, - IRequestFactorySelector requestFactorySelector, - ISSEStreamReaderFactory sseStreamReaderFactory) + IRequestFactorySelector requestFactorySelector) { this.httpClientHandlerFactory = httpClientHandlerFactory ?? throw new ArgumentNullException(nameof(httpClientHandlerFactory)); this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.languageProvider = languageProvider ?? throw new ArgumentNullException(nameof(languageProvider)); this.requestFactorySelector = requestFactorySelector; - this.sseStreamReaderFactory = sseStreamReaderFactory; } public bool IsConnected => GetServerInfo() != null; @@ -127,21 +119,6 @@ await InvokeCheckedRequestAsync> SearchFilesByNameAsync( - string projectKey, - string branch, - string fileName, - CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - request.BranchName = branch; - request.FileName = fileName; - }, - token - ); - public Uri GetViewIssueUrl(string projectKey, string issueKey) { EnsureIsConnected(); @@ -162,18 +139,6 @@ await InvokeCheckedRequestAsync CreateSSEStreamReader(string projectKey, CancellationToken token) - { - var networkStream = await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, - token); - - return sseStreamReaderFactory.Create(networkStream, token); - } - /// /// Creates a new instance of the specified TRequest request, configures and invokes it and returns its response. /// diff --git a/src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs b/src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs deleted file mode 100644 index 7067f68637..0000000000 --- a/src/TestInfrastructure/Helpers/DummySonarQubeIssueFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using SonarQube.Client.Models; - -namespace SonarLint.VisualStudio.Integration.TestInfrastructure.Helpers; - -public static class DummySonarQubeIssueFactory -{ - public static SonarQubeIssue CreateServerIssue(bool isResolved = false) - { - return new SonarQubeIssue("issueKey", default, default, default, default, default, isResolved, default, default, default, default, default); - } -} From b6aa02c26188bc58f1e4e06b6218ef3fe0204e7a Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh Date: Tue, 14 Oct 2025 16:18:41 +0200 Subject: [PATCH 37/38] fix after rebase --- src/EmbeddedSonarAnalyzer.props | 3 +- ...egration.Vsix_Baseline_WithStrongNames.txt | 31 ++++++++----------- ...ation.Vsix_Baseline_WithoutStrongNames.txt | 31 ++++++++----------- .../SonarQube.Client.Tests.csproj | 6 ---- 4 files changed, 28 insertions(+), 43 deletions(-) diff --git a/src/EmbeddedSonarAnalyzer.props b/src/EmbeddedSonarAnalyzer.props index cf46caf535..fa366e179b 100644 --- a/src/EmbeddedSonarAnalyzer.props +++ b/src/EmbeddedSonarAnalyzer.props @@ -9,9 +9,10 @@ 11.4.1.34873 3.19.0.5695 2.30.0.8328 + 1.0.0.89 - 10.34.0.83431 + 10.35.0.83435 1.0.0 diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt index 3f3669778e..867e7faec7 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2025-10-14T13:19:29.6557597Z +# Report date/time: 2025-10-14T14:04:05.5092281Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -51,7 +51,7 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -- 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' @@ -107,12 +107,10 @@ Referenced assemblies: - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'System.Threading.Channels, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' -- 'System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' - 'System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -# Number of references: 28 +# Number of references: 26 --- Assembly: 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' @@ -164,12 +162,9 @@ Assembly: 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=n Relative path: 'SonarLint.VisualStudio.Infrastructure.VS.dll' Referenced assemblies: -- 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.CoreUtility, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Editor, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Interop, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.Shell.15.0, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Shell.Framework, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Text.Data, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' @@ -177,12 +172,11 @@ Referenced assemblies: - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' -# Number of references: 18 +# Number of references: 14 --- Assembly: 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' @@ -285,23 +279,23 @@ Referenced assemblies: # Number of references: 22 --- -Assembly: 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -Relative path: 'SonarLint.VisualStudio.Roslyn.Suppressions.dll' +Assembly: 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +Relative path: 'SonarLint.VisualStudio.RoslynAnalyzerServer.dll' Referenced assemblies: - 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.VisualStudio.Threading, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.Threading, Version=16.10.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' -- 'SonarLint.VisualStudio.ConnectedMode, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -- 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' -- 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' +- 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' # Number of references: 13 --- @@ -331,12 +325,13 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -# Number of references: 10 +# Number of references: 11 --- Assembly: 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt index 58862f0d78..2fa27ec438 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2025-10-14T13:19:29.6557597Z +# Report date/time: 2025-10-14T14:04:05.5092281Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -51,7 +51,7 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -- 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' @@ -107,12 +107,10 @@ Referenced assemblies: - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'System.Threading.Channels, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' -- 'System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' - 'System.Xaml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -# Number of references: 28 +# Number of references: 26 --- Assembly: 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' @@ -164,12 +162,9 @@ Assembly: 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=n Relative path: 'SonarLint.VisualStudio.Infrastructure.VS.dll' Referenced assemblies: -- 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.CoreUtility, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Editor, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Interop, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'Microsoft.VisualStudio.Shell.15.0, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Shell.Framework, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Text.Data, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' @@ -177,12 +172,11 @@ Referenced assemblies: - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' -# Number of references: 18 +# Number of references: 14 --- Assembly: 'SonarLint.VisualStudio.Integration, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' @@ -285,23 +279,23 @@ Referenced assemblies: # Number of references: 22 --- -Assembly: 'SonarLint.VisualStudio.Roslyn.Suppressions, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -Relative path: 'SonarLint.VisualStudio.Roslyn.Suppressions.dll' +Assembly: 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' +Relative path: 'SonarLint.VisualStudio.RoslynAnalyzerServer.dll' Referenced assemblies: - 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -- 'Microsoft.VisualStudio.Threading, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' +- 'Microsoft.VisualStudio.Threading, Version=16.10.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' -- 'SonarLint.VisualStudio.ConnectedMode, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -- 'SonarLint.VisualStudio.Infrastructure.VS, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' -- 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -- 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' +- 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' +- 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' # Number of references: 13 --- @@ -331,12 +325,13 @@ Referenced assemblies: - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.IssueVisualization.Security, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' +- 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'SonarLint.VisualStudio.SLCore, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' -# Number of references: 10 +# Number of references: 11 --- Assembly: 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' diff --git a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj index 67ca3fdf5a..c36ca4054f 100644 --- a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj +++ b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj @@ -30,12 +30,6 @@ - - - TestParallelization.cs - - - From 619d22d087d7fdc1c4c717bee546220b13a1b234 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:39:46 +0200 Subject: [PATCH 38/38] SLVS-2173 Integrate with SLCore branch logic, drop SQClient requests (#6464) [SLVS-2173](https://sonarsource.atlassian.net/browse/SLVS-2173) [SLVS-2173]: https://sonarsource.atlassian.net/browse/SLVS-2173?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- THIRD-PARTY-NOTICES | 40 -- src/CFamily.UnitTests/packages.lock.json | 12 - .../BranchMatcherTests.cs | 134 +++---- .../ServerBranchProviderTests.cs | 234 ++++------- .../StatefulServerBranchProviderTests.cs | 377 ++++++++---------- .../packages.lock.json | 12 - src/ConnectedMode/BranchMatcher.cs | 35 +- src/ConnectedMode/ConnectedModePackage.cs | 10 +- src/ConnectedMode/Resources.Designer.cs | 39 +- src/ConnectedMode/Resources.resx | 14 +- src/ConnectedMode/ServerBranchProvider.cs | 144 ++++--- src/ConnectedMode/SlCoreGitChangeNotifier.cs | 110 +++++ .../StatefulServerBranchProvider.cs | 161 -------- src/ConnectedMode/packages.lock.json | 12 - src/Core.UnitTests/packages.lock.json | 12 - .../Binding/IActiveSolutionBoundTracker.cs | 11 - .../IActiveConfigScopeTracker.cs | 5 +- src/Core/IServerBranchProvider.cs | 46 --- src/Education.UnitTests/packages.lock.json | 12 - .../packages.lock.json | 12 - src/Infrastructure.VS/packages.lock.json | 20 - .../ActiveSolutionBoundTrackerTests.cs | 112 +----- src/Integration.UnitTests/packages.lock.json | 12 - .../packages.lock.json | 12 - ...egration.Vsix_Baseline_WithStrongNames.txt | 5 +- ...ation.Vsix_Baseline_WithoutStrongNames.txt | 5 +- src/Integration.Vsix/app.config | 4 - src/Integration.Vsix/packages.lock.json | 24 -- .../MefServices/ActiveSolutionBoundTracker.cs | 20 +- src/Integration/packages.lock.json | 12 - .../packages.lock.json | 12 - src/IssueViz.Security/packages.lock.json | 12 - src/IssueViz.UnitTests/packages.lock.json | 12 - src/IssueViz/packages.lock.json | 12 - .../packages.lock.json | 12 - .../packages.lock.json | 12 - .../BranchListenerTests.cs | 84 ++-- .../packages.lock.json | 12 - .../Implementation/BranchListener.cs | 31 +- src/SLCore.Listeners/packages.lock.json | 12 - .../State/ActiveConfigScopeTrackerTests.cs | 35 ++ src/SLCore.UnitTests/packages.lock.json | 12 - ...dChangeMatchedSonarProjectBranchParams.cs} | 14 +- src/SLCore/Listener/Branch/IBranchListener.cs | 2 +- .../Listener/Branch/IServerBranchProvider.cs} | 32 +- .../Branch/MatchSonarProjectBranchResponse.cs | 2 +- src/SLCore/SLCoreStrings.Designer.cs | 9 + src/SLCore/SLCoreStrings.resx | 3 + src/SLCore/State/ActiveConfigScopeTracker.cs | 37 +- .../State/ISlCoreGitChangeNotifier.cs} | 18 +- .../IssuesProtobufResponse | Bin 720 -> 0 bytes .../DefaultConfiguration_Configure_Tests.cs | 4 - .../SonarQube.Client.Tests.csproj | 10 - ...onarQubeService_GetProjectBranchesAsync.cs | 109 ----- .../TestResources/IssuesProtobufResponse | Bin 720 -> 0 bytes src/SonarQube.Client.Tests/packages.config | 1 - src/SonarQube.Client.Tests/packages.lock.json | 13 - .../Api/DefaultConfiguration.cs | 6 +- .../Api/V6_60/GetProjectBranchesRequest.cs | 61 --- src/SonarQube.Client/ISonarQubeService.cs | 5 - .../Messages/Protobuf/.gitignore | 1 - .../Messages/Protobuf/README.md | 2 - .../Messages/Protobuf/scanner_input.proto | 56 --- src/SonarQube.Client/SonarQube.Client.csproj | 26 +- src/SonarQube.Client/SonarQubeService.cs | 7 - src/SonarQube.Client/packages.config | 6 - src/SonarQube.Client/packages.lock.json | 12 - src/TestInfrastructure/packages.lock.json | 12 - 68 files changed, 677 insertions(+), 1687 deletions(-) create mode 100644 src/ConnectedMode/SlCoreGitChangeNotifier.cs delete mode 100644 src/ConnectedMode/StatefulServerBranchProvider.cs delete mode 100644 src/Core/IServerBranchProvider.cs rename src/{SonarQube.Client/Api/IGetProjectBranchesRequest.cs => SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs} (72%) rename src/{SonarQube.Client/Models/SonarQubeProjectBranch.cs => SLCore/Listener/Branch/IServerBranchProvider.cs} (58%) rename src/{SonarQube.Client/Messages/Protobuf/constants.proto => SLCore/State/ISlCoreGitChangeNotifier.cs} (77%) delete mode 100644 src/SonarQube.Client.Tests/IssuesProtobufResponse delete mode 100644 src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs delete mode 100644 src/SonarQube.Client.Tests/TestResources/IssuesProtobufResponse delete mode 100644 src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs delete mode 100644 src/SonarQube.Client/Messages/Protobuf/.gitignore delete mode 100644 src/SonarQube.Client/Messages/Protobuf/README.md delete mode 100644 src/SonarQube.Client/Messages/Protobuf/scanner_input.proto delete mode 100644 src/SonarQube.Client/packages.config diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 8ad9088d6c..252018b158 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -345,46 +345,6 @@ Apache License Version 2.0 END OF TERMS AND CONDITIONS - -License notice for Google Protobuf ------------------------------------------------------ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Code generated by the Protocol Buffer compiler is owned by the owner -of the input file used when generating it. This code is not -standalone and requires a support library to be linked with it. This -support library is itself covered by the above license. - -https://github.com/protocolbuffers/protobuf/blob/master/LICENSE - - -License notice for GRPC ------------------------------------------------------ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -https://github.com/grpc/grpc/blob/master/LICENSE - - License notice for Newtonsoft.Json ----------------------------------------------------- The MIT License (MIT) diff --git a/src/CFamily.UnitTests/packages.lock.json b/src/CFamily.UnitTests/packages.lock.json index cd6914598b..0da00f606f 100644 --- a/src/CFamily.UnitTests/packages.lock.json +++ b/src/CFamily.UnitTests/packages.lock.json @@ -116,16 +116,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1401,8 +1391,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/ConnectedMode.UnitTests/BranchMatcherTests.cs b/src/ConnectedMode.UnitTests/BranchMatcherTests.cs index cb7eced55c..b50f7de265 100644 --- a/src/ConnectedMode.UnitTests/BranchMatcherTests.cs +++ b/src/ConnectedMode.UnitTests/BranchMatcherTests.cs @@ -18,17 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.ConnectedMode.UnitTests.LibGit2SharpWrappers; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.SLCore.Listener.Branch; using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests { @@ -39,7 +33,6 @@ public class BranchMatcherTests public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); } @@ -47,26 +40,26 @@ public void MefCtor_CheckIsExported() [DataRow("branch")] [DataRow("Branch")] [TestMethod] - public async Task GetMatchingBranch_ChooseBranchWithSameName(string serverBranchName) + public void GetMatchingBranch_ChooseBranchWithSameName(string serverBranchName) { - var service = CreateSonarQubeService("master", serverBranchName).Object; + var remoteBranches = CreateRemoteBranches("master", serverBranchName); var masterBranch = CreateBranch("master"); var headBranch = CreateBranch("branch"); var repo = CreateRepo(headBranch, masterBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("branch"); } [TestMethod] - public async Task GetMatchingBranch_NoBranchWithSameName_ChooseClosestMatch() + public void GetMatchingBranch_NoBranchWithSameName_ChooseClosestMatch() { - var service = CreateSonarQubeService("master", "dev").Object; + var remoteBranches = CreateRemoteBranches("master", "dev"); var masterCommit = new CommitWrapper(1); var devBranchCommit = new CommitWrapper(2); @@ -78,17 +71,17 @@ public async Task GetMatchingBranch_NoBranchWithSameName_ChooseClosestMatch() var repo = CreateRepo(headBranch, masterBranch, devBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("dev"); } [TestMethod] - public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() + public void GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() { - var service = CreateSonarQubeService("master", "serverbranch").Object; + var remoteBranches = CreateRemoteBranches("master", "serverbranch"); var masterCommit0 = new CommitWrapper(10); var masterCommit1 = new CommitWrapper(11); @@ -106,9 +99,9 @@ public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() var repo = CreateRepo(localBranch, serverBranch, masterBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("serverbranch"); @@ -117,7 +110,7 @@ public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() // * 7 = localbranchCommit2 -> serverBranchCommit3, serverBranchCommit2, serverBranchCommit1, masterCommit3, masterCommit2, masterCommit1, masterCommit0 // * 7 = localbranchCommit1 -> serverBranchCommit3, serverBranchCommit2, serverBranchCommit1, masterCommit3, masterCommit2, masterCommit1, masterCommit0 // * 1 = serverBranchCommit3 -> serverBranchCommit3 - // Total = 7 + 7 + 1 = 13, and closestMatch = 2 + // Total = 7 + 7 + 1 = 15, and closestMatch = 2 ((CommitLogWrapperWithEnumerationCount)serverBranch.Commits).EnumerateCount.Should().Be(15); // ClosestDistance is now 2 so any tries passes those should fail @@ -128,9 +121,9 @@ public async Task GetMatchingBranch_ShorterPathFoundBefore_EarlyOut() } [TestMethod] - public async Task GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() + public void GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() { - var service = CreateSonarQubeService("master", "dev").Object; + var remoteBranches = CreateRemoteBranches("master", "dev"); var masterCommit = new CommitWrapper(1); var devBranchCommit = new CommitWrapper(2); @@ -144,9 +137,9 @@ public async Task GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() var repo = CreateRepo(headBranch, closestBranch, masterBranch, devBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("dev"); } @@ -185,9 +178,9 @@ public async Task GetMatchingBranch_ClosestBranchNotOnTheServer_IgnoreClosest() */ [TestMethod] - public async Task GetMatchingBranch_MultipleCandidate_ChooseClosestMatch() + public void GetMatchingBranch_MultipleCandidate_ChooseClosestMatch() { - var service = CreateSonarQubeService("master", "Branch1", "Branch2").Object; + var remoteBranches = CreateRemoteBranches("master", "Branch1", "Branch2"); var commit1 = new CommitWrapper(1); var commit2 = new CommitWrapper(2); @@ -207,17 +200,17 @@ public async Task GetMatchingBranch_MultipleCandidate_ChooseClosestMatch() var repo = CreateRepo(headBranch: branch3, masterBranch, branch1, branch2); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("branch2"); } [TestMethod] - public async Task GetMatchingBranch_NoMatchingBranch_ChooseMain() + public void GetMatchingBranch_NoMatchingBranch_ChooseMain() { - var service = CreateSonarQubeService("premier", "branch1", "branch2").Object; + var remoteBranches = CreateRemoteBranches("premier", "branch1", "branch2"); var masterCommit = new CommitWrapper(1); var branch1Commit = new CommitWrapper(2); @@ -231,17 +224,17 @@ public async Task GetMatchingBranch_NoMatchingBranch_ChooseMain() var repo = CreateRepo(headBranch, masterBranch, branch1, branch2); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("premier"); } [TestMethod] - public async Task GetMatchingBranch_MultipleMatchingBranchesWithMain_ChooseMain() + public void GetMatchingBranch_MultipleMatchingBranchesWithMain_ChooseMain() { - var service = CreateSonarQubeService("master", "branch1", "branch2").Object; + var remoteBranches = CreateRemoteBranches("master", "branch1", "branch2"); var commit1 = new CommitWrapper(1); var commit2 = new CommitWrapper(2); @@ -256,17 +249,24 @@ public async Task GetMatchingBranch_MultipleMatchingBranchesWithMain_ChooseMain( var repo = CreateRepo(branch3, branch1, branch2, masterBranch); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); result.Should().Be("master"); } [TestMethod] - public async Task GetMatchingBranch_HasShortLivedBranches_IgnoreShortLivedBranch() + public void GetMatchingBranch_HasShortLivedBranches_BranchTypeIsIgnored() { - var service = CreateSonarQubeServiceWithTypes("master", ("branch1", "LONG"), ("branch2", "SHORT")).Object; + // Note: the new RemoteBranch doesn't have a branch type, so we're testing that the algorithm + // works correctly without considering branch types + var remoteBranches = new List + { + new RemoteBranch("master", true), + new RemoteBranch("branch1", false), + new RemoteBranch("branch2", false) + }; var commit1 = new CommitWrapper(1); var commit2 = new CommitWrapper(2); @@ -280,41 +280,23 @@ public async Task GetMatchingBranch_HasShortLivedBranches_IgnoreShortLivedBranch var repo = CreateRepo(branch3, masterBranch, branch1, branch2); - var testSubject = CreateTestSubject(service); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", repo, CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", repo, remoteBranches); - result.Should().Be("branch1"); + result.Should().Be("branch2"); } [TestMethod] - public async Task GetMatchingBranch_RepoHasNoHead_ReturnsNull() + public void GetMatchingBranch_RepoHasNoHead_ReturnsNull() { - var testSubject = CreateTestSubject(CreateSonarQubeService("any").Object); + var testSubject = CreateTestSubject(); - var result = await testSubject.GetMatchingBranch("projectKey", Mock.Of(), CancellationToken.None); + var result = testSubject.GetMatchingBranch("projectKey", Mock.Of(), new List { new RemoteBranch("any", true) }); result.Should().BeNull(); } - [TestMethod] - public async Task GetMatchingBranch_CancellationToken_IsPropagatedToWebClient() - { - var service = CreateSonarQubeService("any"); - - var commit = new CommitWrapper(1); - var branch = CreateBranch("premier", commit); - var repo = CreateRepo(branch); - - var source = new CancellationTokenSource(); - - var testSubject = CreateTestSubject(service.Object); - - _ = await testSubject.GetMatchingBranch("projectKey", repo, source.Token); - - service.Verify(x => x.GetProjectBranchesAsync("projectKey", source.Token), Times.Once); - } - private static IRepository CreateRepo(Branch headBranch, params Branch[] branches) { var branchList = new List @@ -339,33 +321,21 @@ private static BranchWrapper CreateBranch(string branchName, params CommitWrappe private static BranchWrapper CreateBranchWithEnumerationCount(string branchName, params CommitWrapper[] commits) => new(branchName, new CommitLogWrapperWithEnumerationCount(commits)); - private static Mock CreateSonarQubeService(string mainBranch, params string[] branches) + private static List CreateRemoteBranches(string mainBranch, params string[] branches) { - var branchesWithType = branches.Select(b => (b, "BRANCH")).ToArray(); - - return CreateSonarQubeServiceWithTypes(mainBranch, branchesWithType); - } - - private static Mock CreateSonarQubeServiceWithTypes(string mainBranch, params (string branchName, string type)[] branches) - { - var service = new Mock(); - service.Setup(x => x.IsConnected).Returns(true); - - IList remoteBranches = new List(); + var result = new List(); foreach (var branch in branches) { - remoteBranches.Add(new SonarQubeProjectBranch(branch.branchName, false, DateTime.Now, branch.type)); + result.Add(new RemoteBranch(branch, false)); } - remoteBranches.Add(new SonarQubeProjectBranch(mainBranch, true, DateTime.Now, "BRANCH")); - - service.Setup(s => s.GetProjectBranchesAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(remoteBranches)); + result.Add(new RemoteBranch(mainBranch, true)); - return service; + return result; } - private static BranchMatcher CreateTestSubject(ISonarQubeService sonarQubeService) - => new BranchMatcher(sonarQubeService, new TestLogger(logToConsole: true)); + private static BranchMatcher CreateTestSubject() + => new BranchMatcher(new TestLogger(logToConsole: true)); } } diff --git a/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs b/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs index 568615b17f..541672b4f3 100644 --- a/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs +++ b/src/ConnectedMode.UnitTests/ServerBranchProviderTests.cs @@ -18,221 +18,135 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore.Listener.Branch; using SonarLint.VisualStudio.TestInfrastructure; -using SonarQube.Client; -using SonarQube.Client.Models; namespace SonarLint.VisualStudio.ConnectedMode.UnitTests { [TestClass] public class ServerBranchProviderTests { - [TestMethod] - public void MefCtor_CheckIsExported() + private IActiveSolutionBoundTracker activeSolutionBoundTracker; + private IGitWorkspaceService gitWorkspaceService; + private IBranchMatcher branchMatcher; + private TestLogger logger; + private ServerBranchProvider.CreateRepositoryObject createRepoOp; + private ServerBranchProvider testSubject; + private IRepository repo; + + [TestInitialize] + public void TestInitialize() { + activeSolutionBoundTracker = Substitute.For(); + gitWorkspaceService = Substitute.For(); + branchMatcher = Substitute.For(); + logger = Substitute.ForPartsOf(); + createRepoOp = Substitute.For(); + repo = Substitute.For(); + + testSubject = new ServerBranchProvider(activeSolutionBoundTracker, gitWorkspaceService, branchMatcher, logger, createRepoOp); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); - } [TestMethod] - public async Task Get_StandaloneMode_ReturnsNull() + public void Get_StandaloneMode_ReturnsNull() { - var configProvider = CreateConfigProvider(CreateBindingConfig(SonarLintMode.Standalone)); - var gitWorkspace = new Mock(); - var branchMatcher = new Mock(); - var logger = new TestLogger(logToConsole: true); - var sonarQubeService = CreateSonarQubeService(); + SetUpBindingConfiguration(SonarLintMode.Standalone); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("main", true), new RemoteBranch("branch2", false) }; - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - branchMatcher: branchMatcher.Object, - sonarQubeService: sonarQubeService.Object, - logger: logger); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().BeNull(); - - configProvider.VerifyAll(); - gitWorkspace.Invocations.Should().HaveCount(0); - branchMatcher.Invocations.Should().HaveCount(0); - sonarQubeService.Invocations.Should().HaveCount(0); + _ = activeSolutionBoundTracker.Received(1).CurrentConfiguration; + gitWorkspaceService.DidNotReceiveWithAnyArgs().GetRepoRoot(); + branchMatcher.DidNotReceiveWithAnyArgs().GetMatchingBranch(Arg.Any(), Arg.Any(), Arg.Any>()); } [TestMethod] - public async Task Get_NoGitRepo_ReturnsDefaultMainBranch() + public void Get_NoGitRepo_ReturnsDefaultMainBranch() { - var configProvider = CreateConfigProvider(CreateBindingConfig(SonarLintMode.Connected)); - var gitWorkspace = CreateGitWorkspace(repoRootToReturn: null); - - var branchMatcher = new Mock(); - var logger = new TestLogger(logToConsole: true); - var createRepoOp = CreateCreateRepoOp(repository: null); - - var sonarQubeService = CreateSonarQubeService(mainBranchName: "main branch name"); + SetUpBindingConfiguration(SonarLintMode.Connected); + SetUpGitWorkspaceService(null); + createRepoOp.Invoke(Arg.Any()).Returns((IRepository)null); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("main branch name", true), new RemoteBranch("branch2", false) }; - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - sonarQubeService: sonarQubeService.Object, - branchMatcher: branchMatcher.Object, - logger: logger, - createRepoOp: createRepoOp.Object); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().Be("main branch name"); - - configProvider.VerifyAll(); - gitWorkspace.Verify(x => x.GetRepoRoot(), Times.Once); - gitWorkspace.Invocations.Should().HaveCount(1); - branchMatcher.Invocations.Should().HaveCount(0); - createRepoOp.Invocations.Should().HaveCount(0); + _ = activeSolutionBoundTracker.Received(1).CurrentConfiguration; + gitWorkspaceService.Received(1).GetRepoRoot(); + gitWorkspaceService.ReceivedCalls().Should().HaveCount(1); + branchMatcher.DidNotReceiveWithAnyArgs().GetMatchingBranch(Arg.Any(), Arg.Any(), Arg.Any>()); + createRepoOp.DidNotReceiveWithAnyArgs().Invoke(Arg.Any()); } [TestMethod] [DataRow(SonarLintMode.LegacyConnected)] [DataRow(SonarLintMode.Connected)] - public async Task Get_ConnectedModeAndHasGitRepo_HasMatchingBranch_ReturnsExpectedBranch(SonarLintMode mode) + public void Get_ConnectedModeAndHasGitRepo_HasMatchingBranch_ReturnsExpectedBranch(SonarLintMode mode) { - var configProvider = CreateConfigProvider(CreateBindingConfig(mode, "my project key")); - var gitWorkspace = CreateGitWorkspace("c:\\aaa\\reporoot"); - - var branchMatcher = CreateBranchMatcher(branchToReturn: "my matching branch"); - var logger = new TestLogger(logToConsole: true); - - var repo = Mock.Of(); - var createRepoOp = CreateCreateRepoOp(repository: repo); - var sonarQubeService = CreateSonarQubeService(); + SetUpBindingConfiguration(mode, "my project key"); + var repoRoot = "c:\\aaa\\reporoot"; + SetUpGitWorkspaceService(repoRoot); + SetUpBranchMatcher("my matching branch"); + createRepoOp.Invoke(repoRoot).Returns(repo); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("main", true), new RemoteBranch("branch2", false) }; - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - branchMatcher: branchMatcher.Object, - sonarQubeService: sonarQubeService.Object, - logger: logger, - createRepoOp: createRepoOp.Object); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().Be("my matching branch"); logger.AssertPartialOutputStringExists("my matching branch"); - - configProvider.VerifyAll(); - gitWorkspace.Verify(x => x.GetRepoRoot(), Times.Once); - createRepoOp.Verify(x => x.Invoke("c:\\aaa\\reporoot"), Times.Once); - branchMatcher.Verify(x => x.GetMatchingBranch("my project key", repo, It.IsAny()), Times.Once); - - gitWorkspace.Invocations.Should().HaveCount(1); - branchMatcher.Invocations.Should().HaveCount(1); - createRepoOp.Invocations.Should().HaveCount(1); - sonarQubeService.Invocations.Should().HaveCount(0); + _ = activeSolutionBoundTracker.Received(1).CurrentConfiguration; + gitWorkspaceService.Received(1).GetRepoRoot(); + createRepoOp.Received(1).Invoke(repoRoot); + branchMatcher.Received(1).GetMatchingBranch("my project key", repo, branches); } [TestMethod] - public async Task Get_ConnectedModeAndHasGitRepo_NoMatchingBranch_ReturnsDefaultMainBranch() + public void Get_ConnectedModeAndHasGitRepo_NoMatchingBranch_ReturnsDefaultMainBranch() { - var configProvider = CreateConfigProvider(CreateBindingConfig(SonarLintMode.Connected, "my project key")); - var gitWorkspace = CreateGitWorkspace("x:\\"); - - var branchMatcher = CreateBranchMatcher(branchToReturn: null); - var logger = new TestLogger(logToConsole: true); + SetUpBindingConfiguration(SonarLintMode.Connected, "my project key"); + var repoRoot = "x:\\"; + SetUpGitWorkspaceService(repoRoot); + SetUpBranchMatcher(null); + createRepoOp.Invoke(repoRoot).Returns(repo); + var branches = new List { new RemoteBranch("branch1", false), new RemoteBranch("some main branch", true), new RemoteBranch("branch2", false) }; - var repo = Mock.Of(); - var createRepoOp = CreateCreateRepoOp(repository: repo); - var sonarQubeService = CreateSonarQubeService(mainBranchName: "some main branch"); - - var testSubject = CreateTestSubject(configProvider.Object, - gitWorkspace.Object, - branchMatcher: branchMatcher.Object, - sonarQubeService: sonarQubeService.Object, - logger: logger, - createRepoOp: createRepoOp.Object); - - var actual = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + var actual = testSubject.GetServerBranchName(branches); actual.Should().Be("some main branch"); - - branchMatcher.Verify(x => x.GetMatchingBranch("my project key", repo, It.IsAny()), Times.Once); - } - - private static ServerBranchProvider CreateTestSubject( - IConfigurationProvider configurationProvider = null, - IGitWorkspaceService gitWorkspaceService = null, - ISonarQubeService sonarQubeService = null, - IBranchMatcher branchMatcher = null, - ILogger logger = null, - ServerBranchProvider.CreateRepositoryObject createRepoOp = null) - { - configurationProvider ??= Mock.Of(); - gitWorkspaceService ??= Mock.Of(); - sonarQubeService ??= Mock.Of(); - branchMatcher ??= Mock.Of(); - logger ??= new TestLogger(); - createRepoOp ??= (string repoRoot) => null; - - var testSubject = new ServerBranchProvider(configurationProvider, gitWorkspaceService, sonarQubeService, branchMatcher, logger, createRepoOp); - return testSubject; - } - - private static BindingConfiguration CreateBindingConfig(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") - => new(new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), mode, "any dir"); - - private static Mock CreateConfigProvider(BindingConfiguration config = null) - { - config ??= CreateBindingConfig(); - - var configProvider = new Mock(); - configProvider.Setup(x => x.GetConfiguration()).Returns(config); - return configProvider; + branchMatcher.Received(1).GetMatchingBranch("my project key", repo, branches); } - private static Mock CreateGitWorkspace(string repoRootToReturn) + private void SetUpBindingConfiguration(SonarLintMode mode = SonarLintMode.Connected, string projectKey = "any") { - var gitWorkspace = new Mock(); - gitWorkspace.Setup(x => x.GetRepoRoot()).Returns(repoRootToReturn); - return gitWorkspace; - } + var config = new BindingConfiguration( + new BoundServerProject("solution", projectKey, new ServerConnection.SonarCloud("org")), + mode, + "any dir"); - private static Mock CreateCreateRepoOp(IRepository repository) - { - var createOp = new Mock(); - createOp.Setup(x => x.Invoke(It.IsAny())).Returns(repository); - return createOp; + activeSolutionBoundTracker.CurrentConfiguration.Returns(config); } - private static Mock CreateBranchMatcher(string branchToReturn) + private void SetUpGitWorkspaceService(string repoRootToReturn) { - var branchMatcher = new Mock(); - branchMatcher.Setup(x => x.GetMatchingBranch(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(branchToReturn); - return branchMatcher; + gitWorkspaceService.GetRepoRoot().Returns(repoRootToReturn); } - private Mock CreateSonarQubeService(string mainBranchName = "some branch") + private void SetUpBranchMatcher(string branchToReturn) { - var sonarQubeService = new Mock(); - - var serverBranches = new[] - { - new SonarQubeProjectBranch(Guid.NewGuid().ToString(), false, default, "BRANCH"), - new SonarQubeProjectBranch(mainBranchName, true, default, "BRANCH"), - new SonarQubeProjectBranch(Guid.NewGuid().ToString(), false, default, "BRANCH") - }; - - sonarQubeService - .Setup(x => x.GetProjectBranchesAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(serverBranches); - - return sonarQubeService; + branchMatcher.GetMatchingBranch(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(branchToReturn); } } } diff --git a/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs b/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs index 3d065a679e..a6028a3a4d 100644 --- a/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs +++ b/src/ConnectedMode.UnitTests/StatefulServerBranchProviderTests.cs @@ -19,268 +19,215 @@ */ using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.Core.Initialization; using SonarLint.VisualStudio.SLCore; using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Listener.Branch; using SonarLint.VisualStudio.SLCore.Service.Branch; +using SonarLint.VisualStudio.SLCore.State; using SonarLint.VisualStudio.TestInfrastructure; -namespace SonarLint.VisualStudio.ConnectedMode.UnitTests +namespace SonarLint.VisualStudio.ConnectedMode.UnitTests; + +[TestClass] +public class SLCoreGitChangeNotifierTests { - [TestClass] - public class StatefulServerBranchProviderTests + private NoOpThreadHandler threadHandling; + private ISLCoreServiceProvider slCoreServiceProvider; + private TestLogger logger; + private SlCoreGitChangeNotifier testSubject; + private IActiveConfigScopeTracker activeConfigScopeTracker; + private ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService; + private IBoundSolutionGitMonitor gitMonitor; + private IInitializationProcessorFactory initializationProcessorFactory; + + [TestInitialize] + public void TestInitialize() { - private readonly ActiveSolutionBindingEventArgs connectedModeBinding = new(new BindingConfiguration(default, SonarLintMode.Connected, default)); - private readonly ActiveSolutionBindingEventArgs standaloneModeBinding = new(BindingConfiguration.Standalone); - private IThreadHandling threadHandling; - private IActiveSolutionBoundTracker activeSolutionBoundTracker; - private ISLCoreServiceProvider slCoreServiceProvider; - private TestLogger logger; - private IAsyncLockFactory asyncLockFactory; - private StatefulServerBranchProvider testSubject; - private IServerBranchProvider serverBranchProvider; - private IActiveConfigScopeTracker activeConfigScopeTracker; - private ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService; - private IAsyncLock asyncLock; - - [TestInitialize] - public void TestInitialize() - { - serverBranchProvider = Substitute.For(); - activeSolutionBoundTracker = Substitute.For(); - activeConfigScopeTracker = Substitute.For(); - logger = new TestLogger(); - threadHandling = new NoOpThreadHandler(); - slCoreServiceProvider = Substitute.For(); - MockSonarProjectBranchSlCoreService(); - MockAsyncLock(); - testSubject = new StatefulServerBranchProvider(serverBranchProvider, activeSolutionBoundTracker, activeConfigScopeTracker, slCoreServiceProvider, logger, threadHandling, asyncLockFactory); - } - - [TestMethod] - public void MefCtor_CheckIsExported() => - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - - [TestMethod] - public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); - - [TestMethod] - public async Task GetServerBranchNameAsync_WhenCalled_UsesCache() - { - MockGetServerBranchName("Branch"); - - // First call: Should use IServerBranchProvider - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); - serverBranch.Should().Be("Branch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - - // Second call: Should use cache - serverBranchProvider.SetBranchNameToReturn("branch name that should not be returned - should use cached value"); - serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + activeConfigScopeTracker = Substitute.For(); + logger = Substitute.ForPartsOf(); + threadHandling = Substitute.ForPartsOf(); + slCoreServiceProvider = Substitute.For(); + gitMonitor = Substitute.For(); - // Not expecting any more threading calls since using the cache - serverBranch.Should().Be("Branch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - } - - [TestMethod] - public async Task GetServerBranchNameAsync_RunsOnBackgroundThread() - { - MockGetServerBranchName("Branch"); - var mockedThreadHandling = MockThreadHandling(); - testSubject = new StatefulServerBranchProvider(serverBranchProvider, activeSolutionBoundTracker, activeConfigScopeTracker, slCoreServiceProvider, logger, mockedThreadHandling, - asyncLockFactory); + MockSonarProjectBranchSlCoreService(); + } - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public void WhenInitialized_EventHandlersAreRegistered() + { + CreateAndInitializeTestSubject(); - await mockedThreadHandling.Received(1).RunOnBackgroundThread(Arg.Any>>()); - mockedThreadHandling.ReceivedCalls().Should().HaveCount(1); - serverBranch.Should().Be("Branch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - } + var initializationDependencies = new IRequireInitialization[] { gitMonitor }; + initializationProcessorFactory.Received(1).Create(Arg.Is>(x => x.SequenceEqual(initializationDependencies)), Arg.Any>()); - [TestMethod] - public async Task GetServerBranchNameAsync_WhenCalled_UpdatesCacheThreadSafe() + Received.InOrder(() => { - MockGetServerBranchName("Branch"); - - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); + threadHandling.RunOnUIThreadAsync(Arg.Any()); + gitMonitor.HeadChanged += Arg.Any(); + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Arg.Any>(); + }); + } - serverBranch.Should().Be("Branch"); - await asyncLock.Received(1).AcquireAsync(); - } + [TestMethod] + public void OnHeadChanged_NotifiesSlCoreAboutBranchChange() + { + CreateAndInitializeTestSubject(); - [DataTestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task GetServerBranchNameAsync_PreSolutionBindingChanged_CacheIsCleared(bool isConnected) => - await TestEffectOfRaisingEventOnCacheAsync(RaisePreSolutionBindingUpdated, eventArg: isConnected ? connectedModeBinding : standaloneModeBinding, shouldClearCache: true); + const string expectedConfigScopeId = "expected-id"; + var configScope = new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId); + activeConfigScopeTracker.Current.Returns(configScope); + MockSonarProjectBranchSlCoreService(); - [TestMethod] - public async Task GetServerBranchNameAsync_PreSolutionBindingUpdated_CacheIsCleared() => await TestEffectOfRaisingEventOnCacheAsync(RaisePreSolutionBindingUpdated, shouldClearCache: true); + gitMonitor.HeadChanged += Raise.EventWith(gitMonitor, EventArgs.Empty); - [TestMethod] - public async Task GetServerBranchNameAsync_SolutionBindingChanged_CacheIsNotCleared() => await TestEffectOfRaisingEventOnCacheAsync(RaiseSolutionBindingUpdated, shouldClearCache: false); + sonarProjectBranchSlCoreService.Received(1) + .DidVcsRepositoryChange(Arg.Is(p => p.configurationScopeId == expectedConfigScopeId)); + } - [TestMethod] - public async Task GetServerBranchNameAsync_SolutionBindingUpdated_CacheIsNotCleared() => await TestEffectOfRaisingEventOnCacheAsync(RaiseSolutionBindingUpdated, shouldClearCache: false); + [TestMethod] + public void OnHeadChanged_WhenConfigScopeIsNull_DoesNotNotifySlCore() + { + CreateAndInitializeTestSubject(); - [TestMethod] - public void NotifySlCoreBranchChange_BindingChanged_Connected_CallsDidVcsRepositoryChangeWithCorrectId() - { - const string expectedConfigScopeId = "expected-id"; - activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId)); - MockSonarProjectBranchSlCoreService(); - MockGetServerBranchName("OriginalBranch"); + activeConfigScopeTracker.Current.Returns((Core.ConfigurationScope.ConfigurationScope)null); + MockSonarProjectBranchSlCoreService(); - RaisePreSolutionBindingUpdated(connectedModeBinding); + gitMonitor.HeadChanged += Raise.EventWith(gitMonitor, EventArgs.Empty); - sonarProjectBranchSlCoreService.Received(1) - .DidVcsRepositoryChange(Arg.Is(p => p.configurationScopeId == expectedConfigScopeId)); - } + sonarProjectBranchSlCoreService.DidNotReceive() + .DidVcsRepositoryChange(Arg.Any()); + } - [TestMethod] - public void NotifySlCoreBranchChange_BindingChanged_Standalone_Ignores() - { - MockGetServerBranchName("OriginalBranch"); + [TestMethod] + public void OnHeadChanged_WhenServiceProviderFails_LogsError() + { + CreateAndInitializeTestSubject(); - RaisePreSolutionBindingUpdated(standaloneModeBinding); + const string expectedConfigScopeId = "expected-id"; + var configScope = new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId); + activeConfigScopeTracker.Current.Returns(configScope); + MockSonarProjectBranchSlCoreService(wasFound: false); - activeConfigScopeTracker.ReceivedCalls().Should().HaveCount(1); // no other calls - slCoreServiceProvider.ReceivedCalls().Should().HaveCount(1); // no other calls - } + gitMonitor.HeadChanged += Raise.EventWith(gitMonitor, EventArgs.Empty); - [TestMethod] - public void NotifySlCoreBranchChange_BindingUpdated_CallsDidVcsRepositoryChangeWithCorrectId() - { - const string expectedConfigScopeId = "expected-id"; - activeConfigScopeTracker.Current.Returns(new Core.ConfigurationScope.ConfigurationScope(expectedConfigScopeId)); - MockSonarProjectBranchSlCoreService(); - MockGetServerBranchName("OriginalBranch"); + logger.AssertPartialOutputStringExists(SLCoreStrings.ServiceProviderNotInitialized); + sonarProjectBranchSlCoreService.DidNotReceive() + .DidVcsRepositoryChange(Arg.Any()); + } - RaisePreSolutionBindingUpdated(null); + [TestMethod] + public void OnConfigScopeChanged_WhenDefinitionChanges_RefreshesGitMonitor() + { + CreateAndInitializeTestSubject(); - sonarProjectBranchSlCoreService.Received(1).DidVcsRepositoryChange(Arg.Is(p => p.configurationScopeId == expectedConfigScopeId)); - } + var args = new ConfigurationScopeChangedEventArgs(true); + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith(activeConfigScopeTracker, args); - [TestMethod] - public void NotifySlCoreBranchChange_BindingChanged_WhenServiceProviderReturnsFalse_LogsError() - { - MockSonarProjectBranchSlCoreService(wasFound: false); - MockGetServerBranchName("OriginalBranch"); + gitMonitor.Received(1).Refresh(); + } - RaisePreSolutionBindingUpdated(connectedModeBinding); + [TestMethod] + public void OnConfigScopeChanged_WhenDefinitionDoesNotChange_DoesNotRefreshGitMonitor() + { + CreateAndInitializeTestSubject(); - logger.AssertPartialOutputStringExists(SLCoreStrings.ServiceProviderNotInitialized); - slCoreServiceProvider.ReceivedCalls().Should().HaveCount(1); // no other calls - } + var args = new ConfigurationScopeChangedEventArgs(false); + activeConfigScopeTracker.CurrentConfigurationScopeChanged += Raise.EventWith(activeConfigScopeTracker, args); - [TestMethod] - public void NotifySlCoreBranchChange_BindingUpdated_WhenServiceProviderReturnsFalse_LogsError() - { - MockSonarProjectBranchSlCoreService(wasFound: false); - MockGetServerBranchName("OriginalBranch"); + gitMonitor.DidNotReceive().Refresh(); + } - RaisePreSolutionBindingUpdated(null); + [TestMethod] + public void Dispose_WhenInitialized_UnhooksEventHandlers() + { + CreateAndInitializeTestSubject(); - logger.AssertPartialOutputStringExists(SLCoreStrings.ServiceProviderNotInitialized); - slCoreServiceProvider.ReceivedCalls().Should().HaveCount(1); // no other calls - } + testSubject.Dispose(); + testSubject.Dispose(); + testSubject.Dispose(); - [TestMethod] - public void Dispose_UnhooksEventHandlers() - { - testSubject.Dispose(); + gitMonitor.ReceivedWithAnyArgs(1).HeadChanged -= default; + activeConfigScopeTracker.ReceivedWithAnyArgs(1).CurrentConfigurationScopeChanged -= default; + } - // Should only unhook the "Pre-" event handlers - activeSolutionBoundTracker.Received(1).PreSolutionBindingChanged -= Arg.Any>(); - activeSolutionBoundTracker.Received(1).PreSolutionBindingUpdated -= Arg.Any(); + [TestMethod] + public void Dispose_WhenNotInitialized_DoesNotExecute() + { + CreateUninitializedTestSubject(out var barrier); - activeSolutionBoundTracker.DidNotReceive().SolutionBindingChanged -= Arg.Any>(); - activeSolutionBoundTracker.DidNotReceive().SolutionBindingUpdated -= Arg.Any(); - } + CheckDisposed(); - private void MockGetServerBranchName(string branchName) => serverBranchProvider.GetServerBranchNameAsync(Arg.Any()).Returns(branchName); + barrier.SetResult(1); + testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); + testSubject.InitializationProcessor.IsFinalized.Should().BeTrue(); + CheckDisposed(); - private void MockAsyncLock() + void CheckDisposed() { - asyncLock = Substitute.For(); - asyncLockFactory = Substitute.For(); - asyncLockFactory.Create().Returns(asyncLock); - } + var act = () => testSubject.Dispose(); - private async Task TestEffectOfRaisingEventOnCacheAsync( - Action eventAction, - bool shouldClearCache, - EventArgs eventArg = null) - { - MockGetServerBranchName("OriginalBranch"); - - //first call: Should use IServerBranchProvider - var serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); - - serverBranch.Should().Be("OriginalBranch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - - serverBranchProvider.SetBranchNameToReturn("NewBranch"); - - // Raise event - should *not* trigger clearing the cache - eventAction(eventArg); - - //second call: may or may not use the cache - serverBranch = await testSubject.GetServerBranchNameAsync(CancellationToken.None); - - if (shouldClearCache) - { - serverBranch.Should().Be("NewBranch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(2); - asyncLock.Received(1).Acquire(); - } - else - { - serverBranch.Should().Be("OriginalBranch"); - serverBranchProvider.VerifyGetServerBranchNameCalled(1); - asyncLock.DidNotReceive().Acquire(); - } + act.Should().NotThrow(); + gitMonitor.DidNotReceiveWithAnyArgs().HeadChanged += default; + gitMonitor.DidNotReceiveWithAnyArgs().HeadChanged -= default; + activeConfigScopeTracker.DidNotReceiveWithAnyArgs().CurrentConfigurationScopeChanged += default; + activeConfigScopeTracker.DidNotReceiveWithAnyArgs().CurrentConfigurationScopeChanged -= default; } + } - private void RaisePreSolutionBindingUpdated(EventArgs eventArg) => activeSolutionBoundTracker.PreSolutionBindingUpdated += Raise.EventWith(null, eventArg); - - private void RaiseSolutionBindingUpdated(EventArgs eventArg) => activeSolutionBoundTracker.SolutionBindingUpdated += Raise.EventWith(null, eventArg); - - private void MockSonarProjectBranchSlCoreService(bool wasFound = true) + private void MockSonarProjectBranchSlCoreService(bool wasFound = true) + { + sonarProjectBranchSlCoreService = Substitute.For(); + slCoreServiceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService _).Returns(x => { - sonarProjectBranchSlCoreService = Substitute.For(); - slCoreServiceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService _).Returns(x => - { - x[0] = sonarProjectBranchSlCoreService; - return wasFound; - }); - } + x[0] = sonarProjectBranchSlCoreService; + return wasFound; + }); + } - private IThreadHandling MockThreadHandling() - { - var mockedThreadHandling = Substitute.For(); - mockedThreadHandling.RunOnBackgroundThread(Arg.Any>>()).Returns(info => info.Arg>>()()); - return mockedThreadHandling; - } + private void CreateUninitializedTestSubject(out TaskCompletionSource barrier) + { + var tcs = barrier = new TaskCompletionSource(); + initializationProcessorFactory = MockableInitializationProcessor.CreateFactory( + threadHandling, + logger, + processor => MockableInitializationProcessor.ConfigureWithWait(processor, tcs)); + + testSubject = new SlCoreGitChangeNotifier( + activeConfigScopeTracker, + slCoreServiceProvider, + gitMonitor, + logger, + threadHandling, + initializationProcessorFactory); } - internal static class StatefulServerBranchProviderTestsExtensions + private void CreateAndInitializeTestSubject() { - public static void VerifyGetServerBranchNameCalled(this IServerBranchProvider serverBranchProvider, int times) => - serverBranchProvider.Received(times).GetServerBranchNameAsync(Arg.Any()); + initializationProcessorFactory = MockableInitializationProcessor.CreateFactory(threadHandling, logger); + + testSubject = new SlCoreGitChangeNotifier( + activeConfigScopeTracker, + slCoreServiceProvider, + gitMonitor, + logger, + threadHandling, + initializationProcessorFactory); - public static void SetBranchNameToReturn(this IServerBranchProvider serverBranchProvider, string branchName) => - serverBranchProvider.GetServerBranchNameAsync(Arg.Any()).Returns(branchName); + testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); } } diff --git a/src/ConnectedMode.UnitTests/packages.lock.json b/src/ConnectedMode.UnitTests/packages.lock.json index eb0a0b7c95..d252cde329 100644 --- a/src/ConnectedMode.UnitTests/packages.lock.json +++ b/src/ConnectedMode.UnitTests/packages.lock.json @@ -162,16 +162,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1562,8 +1552,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/ConnectedMode/BranchMatcher.cs b/src/ConnectedMode/BranchMatcher.cs index 2684f2963e..dadb3cf145 100644 --- a/src/ConnectedMode/BranchMatcher.cs +++ b/src/ConnectedMode/BranchMatcher.cs @@ -18,20 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.ETW; -using SonarQube.Client; +using SonarLint.VisualStudio.SLCore.Listener.Branch; namespace SonarLint.VisualStudio.ConnectedMode { - public interface IBranchMatcher + internal interface IBranchMatcher { /// /// Calculates the Sonar server branch that is the closest match to the head branch. @@ -43,7 +38,7 @@ public interface IBranchMatcher /// This is the same algorithm as the other SonarLint flavours (?except in how we treated branch histories. /// We treat branch histories as series of linear commits ordered by time, even if there are merges). /// - Task GetMatchingBranch(string projectKey, IRepository gitRepo, CancellationToken token); + string GetMatchingBranch(string projectKey, IRepository gitRepo, List branches); } [Export(typeof(IBranchMatcher))] @@ -51,30 +46,23 @@ public interface IBranchMatcher [PartCreationPolicy(CreationPolicy.Shared)] internal class BranchMatcher : IBranchMatcher { - private const string shortBranchType = "SHORT"; - - private readonly ISonarQubeService sonarQubeService; private readonly ILogger logger; [ImportingConstructor] - public BranchMatcher(ISonarQubeService sonarQubeService, ILogger logger) + public BranchMatcher(ILogger logger) { - this.sonarQubeService = sonarQubeService; this.logger = logger; } - public async Task GetMatchingBranch(string projectKey, IRepository gitRepo, CancellationToken token) + public string GetMatchingBranch(string projectKey, IRepository gitRepo, List branches) { - Debug.Assert(sonarQubeService.IsConnected, - "Not expecting GetMatchingBranch to be called unless we are in Connected Mode"); - logger.LogVerbose(Resources.BranchMapper_CalculatingServerBranch_Started); string closestBranch; try { CodeMarkers.Instance.GetMatchingBranchStart(projectKey); - closestBranch = await DoGetMatchingBranch(projectKey, gitRepo, token); + closestBranch = DoGetMatchingBranch(gitRepo, branches); } finally { @@ -85,7 +73,9 @@ public async Task GetMatchingBranch(string projectKey, IRepository gitRe return closestBranch; } - private async Task DoGetMatchingBranch(string projectKey, IRepository gitRepo, CancellationToken token) + private string DoGetMatchingBranch( + IRepository gitRepo, + List branches) { var head = gitRepo.Head; @@ -95,9 +85,8 @@ private async Task DoGetMatchingBranch(string projectKey, IRepository gi return null; } - var remoteBranches = (await sonarQubeService.GetProjectBranchesAsync(projectKey, token)).Where(b => b.Type != shortBranchType); - if (remoteBranches.Any(rb => string.Equals(rb.Name, head.FriendlyName, StringComparison.InvariantCultureIgnoreCase))) + if (branches.Any(rb => string.Equals(rb.Name, head.FriendlyName, StringComparison.InvariantCultureIgnoreCase))) { logger.LogVerbose(Resources.BranchMapper_Match_SameSonarBranchName, head.FriendlyName); return head.FriendlyName; @@ -109,7 +98,7 @@ private async Task DoGetMatchingBranch(string projectKey, IRepository gi Lazy headCommits = new Lazy(() => head.Commits.ToArray()); logger.LogVerbose(Resources.BranchMapper_CheckingSonarBranches); - foreach (var remoteBranch in remoteBranches) + foreach (var remoteBranch in branches) { var localBranch = gitRepo.Branches.FirstOrDefault(r => string.Equals(r.FriendlyName, remoteBranch.Name, StringComparison.InvariantCultureIgnoreCase)); @@ -128,7 +117,7 @@ private async Task DoGetMatchingBranch(string projectKey, IRepository gi if (closestBranch is null) { logger.LogVerbose(Resources.BranchMapper_NoMatchingBranchFound); - closestBranch = remoteBranches.First(rb => rb.IsMain).Name; + closestBranch = branches.First(rb => rb.IsMain).Name; } return closestBranch; diff --git a/src/ConnectedMode/ConnectedModePackage.cs b/src/ConnectedMode/ConnectedModePackage.cs index a27a91241d..139c4541d9 100644 --- a/src/ConnectedMode/ConnectedModePackage.cs +++ b/src/ConnectedMode/ConnectedModePackage.cs @@ -22,10 +22,10 @@ using System.Runtime.InteropServices; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.ConnectedMode.Hotspots; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; +using SonarLint.VisualStudio.SLCore.State; using Task = System.Threading.Tasks.Task; namespace SonarLint.VisualStudio.ConnectedMode @@ -34,11 +34,12 @@ namespace SonarLint.VisualStudio.ConnectedMode [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [ProvideAutoLoad(BoundSolutionUIContext.GuidString, PackageAutoLoadFlags.BackgroundLoad)] [Guid("dd3427e0-7bb2-4a51-b00a-ddae2c32c7ef")] - public sealed class ConnectedModePackage : AsyncPackage + public sealed class ConnectedModePackage : AsyncPackage, IDisposable { private IHotspotDocumentClosedHandler hotspotDocumentClosedHandler; private IHotspotSolutionClosedHandler hotspotSolutionClosedHandler; private ILocalHotspotStoreMonitor hotspotStoreMonitor; + private ISlCoreGitChangeNotifier slCoreGitChangeNotifier; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { @@ -48,6 +49,8 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke var logger = componentModel.GetService(); logger.WriteLine(Resources.Package_Initializing); + slCoreGitChangeNotifier = componentModel.GetService(); + await slCoreGitChangeNotifier.InitializationProcessor.InitializeAsync(); hotspotDocumentClosedHandler = componentModel.GetService(); @@ -56,8 +59,11 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke hotspotStoreMonitor = componentModel.GetService(); await hotspotStoreMonitor.InitializeAsync(); + logger.WriteLine(Resources.Package_Initialized); } + public void Dispose() => + slCoreGitChangeNotifier.Dispose(); } } diff --git a/src/ConnectedMode/Resources.Designer.cs b/src/ConnectedMode/Resources.Designer.cs index 3028bdd85a..6c2029fd8f 100644 --- a/src/ConnectedMode/Resources.Designer.cs +++ b/src/ConnectedMode/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -601,16 +600,7 @@ internal static string SharedBindingConfigProvider_SharedFolderNotFound { } /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Binding changed -> cache cleared. - /// - internal static string StatefulBranchProvider_BindingChanged { - get { - return ResourceManager.GetString("StatefulBranchProvider_BindingChanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Binding updated -> cache cleared. + /// Looks up a localized string similar to Git HEAD changed, updating SLCore. /// internal static string StatefulBranchProvider_BindingUpdated { get { @@ -618,33 +608,6 @@ internal static string StatefulBranchProvider_BindingUpdated { } } - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Cache hit. - /// - internal static string StatefulBranchProvider_CacheHit { - get { - return ResourceManager.GetString("StatefulBranchProvider_CacheHit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Cache miss. Recalculating server branch mapping.... - /// - internal static string StatefulBranchProvider_CacheMiss { - get { - return ResourceManager.GetString("StatefulBranchProvider_CacheMiss", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [ConnectedMode/BranchMapping] Closest Sonar server branch: {0}. - /// - internal static string StatefulBranchProvider_ReturnValue { - get { - return ResourceManager.GetString("StatefulBranchProvider_ReturnValue", resourceCulture); - } - } - /// /// Looks up a localized string similar to Finished fetching suppressions. All issues suppressed: {0}. /// diff --git a/src/ConnectedMode/Resources.resx b/src/ConnectedMode/Resources.resx index 74bccefde2..0ba5bda1fd 100644 --- a/src/ConnectedMode/Resources.resx +++ b/src/ConnectedMode/Resources.resx @@ -175,20 +175,8 @@ [ConnectedMode] Initializing package... - - [ConnectedMode/BranchMapping] Binding changed -> cache cleared - - [ConnectedMode/BranchMapping] Binding updated -> cache cleared - - - [ConnectedMode/BranchMapping] Cache hit - - - [ConnectedMode/BranchMapping] Cache miss. Recalculating server branch mapping... - - - [ConnectedMode/BranchMapping] Closest Sonar server branch: {0} + Git HEAD changed, updating SLCore [ActionRunner] Cancelling current operation... diff --git a/src/ConnectedMode/ServerBranchProvider.cs b/src/ConnectedMode/ServerBranchProvider.cs index 7ed9769548..f8a922fded 100644 --- a/src/ConnectedMode/ServerBranchProvider.cs +++ b/src/ConnectedMode/ServerBranchProvider.cs @@ -19,100 +19,92 @@ */ using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LibGit2Sharp; using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Binding; -using SonarQube.Client; +using SonarLint.VisualStudio.SLCore.Listener.Branch; -namespace SonarLint.VisualStudio.ConnectedMode +namespace SonarLint.VisualStudio.ConnectedMode; + +[Export(typeof(IServerBranchProvider))] +internal class ServerBranchProvider : IServerBranchProvider { - [Export(typeof(IServerBranchProvider))] - internal class ServerBranchProvider : IServerBranchProvider + /// + /// Factory method to create and return an instance. + /// + /// Only used for testing + internal delegate IRepository CreateRepositoryObject(string repoRootPath); + + private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; + private readonly IGitWorkspaceService gitWorkspaceService; + private readonly IBranchMatcher branchMatcher; + private readonly ILogger logger; + private readonly CreateRepositoryObject createRepo; + + [ImportingConstructor] + public ServerBranchProvider( + IActiveSolutionBoundTracker activeSolutionBoundTracker, + IGitWorkspaceService gitWorkspaceService, + IBranchMatcher branchMatcher, + ILogger logger) + : this(activeSolutionBoundTracker, gitWorkspaceService, branchMatcher, logger, DoCreateRepo) { - /// - /// Factory method to create and return an instance. - /// - /// Only used for testing - internal delegate IRepository CreateRepositoryObject(string repoRootPath); - - private readonly IConfigurationProvider configurationProvider; - private readonly IGitWorkspaceService gitWorkspaceService; - private readonly ISonarQubeService sonarQubeService; - private readonly IBranchMatcher branchMatcher; - private readonly ILogger logger; - private readonly CreateRepositoryObject createRepo; - - [ImportingConstructor] - public ServerBranchProvider(IConfigurationProvider configurationProvider, - IGitWorkspaceService gitWorkspaceService, - ISonarQubeService sonarQubeService, - IBranchMatcher branchMatcher, - ILogger logger) - : this(configurationProvider, gitWorkspaceService, sonarQubeService, branchMatcher, logger, DoCreateRepo) - { - } - - internal /* for testing */ ServerBranchProvider(IConfigurationProvider configurationProvider, - IGitWorkspaceService gitWorkspaceService, - ISonarQubeService sonarQubeService, - IBranchMatcher branchMatcher, - ILogger logger, - CreateRepositoryObject createRepo) - { - this.configurationProvider = configurationProvider; - this.gitWorkspaceService = gitWorkspaceService; - this.sonarQubeService = sonarQubeService; - this.branchMatcher = branchMatcher; - this.logger = logger; - this.createRepo = createRepo; - } - - public async Task GetServerBranchNameAsync(CancellationToken token) - { - var config = configurationProvider.GetConfiguration(); + } - if (config.Mode == SonarLintMode.Standalone) - { - logger.LogVerbose(Resources.BranchProvider_NotInConnectedMode); - return null; - } + internal /* for testing */ ServerBranchProvider( + IActiveSolutionBoundTracker activeSolutionBoundTracker, + IGitWorkspaceService gitWorkspaceService, + IBranchMatcher branchMatcher, + ILogger logger, + CreateRepositoryObject createRepo) + { + this.activeSolutionBoundTracker = activeSolutionBoundTracker; + this.gitWorkspaceService = gitWorkspaceService; + this.branchMatcher = branchMatcher; + this.logger = logger; + this.createRepo = createRepo; + } - var matchingBranchName = await CalculateMatchingBranchAsync(config, token); + public string GetServerBranchName(List branches) + { + var config = activeSolutionBoundTracker.CurrentConfiguration; - if (matchingBranchName == null) - { - logger.LogVerbose(Resources.BranchProvider_FailedToCalculateMatchingBranch); + if (config.Mode == SonarLintMode.Standalone) + { + logger.LogVerbose(Resources.BranchProvider_NotInConnectedMode); + return null; + } - var remoteBranches = await sonarQubeService.GetProjectBranchesAsync(config.Project.ServerProjectKey, token); - matchingBranchName = remoteBranches.First(rb => rb.IsMain).Name; - } + var matchingBranchName = CalculateMatchingBranch(config, branches); - Debug.Assert(matchingBranchName != null); + if (matchingBranchName == null) + { + logger.LogVerbose(Resources.BranchProvider_FailedToCalculateMatchingBranch); - logger.WriteLine(Resources.BranchProvider_MatchingServerBranchName, matchingBranchName); - return matchingBranchName; + matchingBranchName = branches.First(rb => rb.IsMain).Name; } - private async Task CalculateMatchingBranchAsync(BindingConfiguration config, CancellationToken token) - { - var gitRepoRoot = gitWorkspaceService.GetRepoRoot(); + Debug.Assert(matchingBranchName != null); - if (gitRepoRoot == null) - { - logger.LogVerbose(Resources.BranchProvider_CouldNotDetectGitRepo); - return null; - } + logger.WriteLine(Resources.BranchProvider_MatchingServerBranchName, matchingBranchName); + return matchingBranchName; + } - var repo = createRepo(gitRepoRoot); - var branchName = await branchMatcher.GetMatchingBranch(config.Project.ServerProjectKey, repo, token); + private string CalculateMatchingBranch(BindingConfiguration config, List branches) + { + var gitRepoRoot = gitWorkspaceService.GetRepoRoot(); - return branchName; + if (gitRepoRoot == null) + { + logger.LogVerbose(Resources.BranchProvider_CouldNotDetectGitRepo); + return null; } - private static IRepository DoCreateRepo(string repoRootPath) => new Repository(repoRootPath); + var repo = createRepo(gitRepoRoot); + var branchName = branchMatcher.GetMatchingBranch(config.Project.ServerProjectKey, repo, branches); + + return branchName; } + + private static IRepository DoCreateRepo(string repoRootPath) => new Repository(repoRootPath); } diff --git a/src/ConnectedMode/SlCoreGitChangeNotifier.cs b/src/ConnectedMode/SlCoreGitChangeNotifier.cs new file mode 100644 index 0000000000..f13b92d653 --- /dev/null +++ b/src/ConnectedMode/SlCoreGitChangeNotifier.cs @@ -0,0 +1,110 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Threading; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Core.Initialization; +using SonarLint.VisualStudio.SLCore; +using SonarLint.VisualStudio.SLCore.Core; +using SonarLint.VisualStudio.SLCore.Service.Branch; +using SonarLint.VisualStudio.SLCore.State; + +namespace SonarLint.VisualStudio.ConnectedMode; + +[Export(typeof(ISlCoreGitChangeNotifier))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal sealed class SlCoreGitChangeNotifier : ISlCoreGitChangeNotifier +{ + private readonly IActiveConfigScopeTracker activeConfigScopeTracker; + private readonly ISLCoreServiceProvider serviceProvider; + private readonly IBoundSolutionGitMonitor gitMonitor; + private readonly ILogger logger; + private readonly IThreadHandling threadHandling; + private bool disposed; + + [ImportingConstructor] + public SlCoreGitChangeNotifier( + IActiveConfigScopeTracker activeConfigScopeTracker, + ISLCoreServiceProvider serviceProvider, + IBoundSolutionGitMonitor gitMonitor, + ILogger logger, + IThreadHandling threadHandling, + IInitializationProcessorFactory initializationProcessorFactory) + { + this.activeConfigScopeTracker = activeConfigScopeTracker; + this.serviceProvider = serviceProvider; + this.gitMonitor = gitMonitor; + this.logger = logger; + this.threadHandling = threadHandling; + + InitializationProcessor = initializationProcessorFactory.Create([gitMonitor], + _ => threadHandling.RunOnUIThreadAsync(() => + { + if (disposed) + { + return; + } + gitMonitor.HeadChanged += GitMonitor_OnHeadChanged; + activeConfigScopeTracker.CurrentConfigurationScopeChanged += ActiveConfigScopeTracker_OnCurrentConfigurationScopeChanged; + })); + } + + private void ActiveConfigScopeTracker_OnCurrentConfigurationScopeChanged(object sender, ConfigurationScopeChangedEventArgs e) + { + if (e.DefinitionChanged) + { + gitMonitor.Refresh(); + } + } + + private void GitMonitor_OnHeadChanged(object sender, EventArgs e) => + threadHandling.RunOnBackgroundThread(() => + { + if (!serviceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService)) + { + logger.LogVerbose(SLCoreStrings.ServiceProviderNotInitialized); + return; + } + if (activeConfigScopeTracker.Current == null) + { + return; + } + sonarProjectBranchSlCoreService.DidVcsRepositoryChange(new DidVcsRepositoryChangeParams(activeConfigScopeTracker.Current.Id)); + }).Forget(); + + public void Dispose() + { + if (disposed) + { + return; + } + + if (InitializationProcessor.IsFinalized) + { + gitMonitor.HeadChanged -= GitMonitor_OnHeadChanged; + activeConfigScopeTracker.CurrentConfigurationScopeChanged -= ActiveConfigScopeTracker_OnCurrentConfigurationScopeChanged; + } + disposed = true; + } + + public IInitializationProcessor InitializationProcessor { get; } +} diff --git a/src/ConnectedMode/StatefulServerBranchProvider.cs b/src/ConnectedMode/StatefulServerBranchProvider.cs deleted file mode 100644 index 8dbf9aca17..0000000000 --- a/src/ConnectedMode/StatefulServerBranchProvider.cs +++ /dev/null @@ -1,161 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Binding; -using SonarLint.VisualStudio.Core.ConfigurationScope; -using SonarLint.VisualStudio.Core.Synchronization; -using SonarLint.VisualStudio.SLCore; -using SonarLint.VisualStudio.SLCore.Core; -using SonarLint.VisualStudio.SLCore.Service.Branch; - -namespace SonarLint.VisualStudio.ConnectedMode -{ - [Export(typeof(IStatefulServerBranchProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - internal sealed class StatefulServerBranchProvider : IStatefulServerBranchProvider, IDisposable - { - private readonly IServerBranchProvider serverBranchProvider; - private readonly IActiveSolutionBoundTracker activeSolutionBoundTracker; - private readonly IActiveConfigScopeTracker activeConfigScopeTracker; - private readonly ISLCoreServiceProvider serviceProvider; - private readonly ILogger logger; - private readonly IThreadHandling threadHandling; - private bool disposedValue; - private string selectedBranch; - private readonly IAsyncLock asyncLock; - - [ImportingConstructor] - public StatefulServerBranchProvider( - IServerBranchProvider serverBranchProvider, - IActiveSolutionBoundTracker activeSolutionBoundTracker, - IActiveConfigScopeTracker activeConfigScopeTracker, - ISLCoreServiceProvider serviceProvider, - ILogger logger, - IThreadHandling threadHandling, - IAsyncLockFactory asyncLockFactory) - { - this.serverBranchProvider = serverBranchProvider; - this.activeSolutionBoundTracker = activeSolutionBoundTracker; - this.activeConfigScopeTracker = activeConfigScopeTracker; - this.serviceProvider = serviceProvider; - this.logger = logger; - this.threadHandling = threadHandling; - asyncLock = asyncLockFactory.Create(); - - activeSolutionBoundTracker.PreSolutionBindingUpdated += OnPreSolutionBindingUpdated; - activeSolutionBoundTracker.PreSolutionBindingChanged += OnPreSolutionBindingChanged; - } - - private void OnPreSolutionBindingUpdated(object sender, EventArgs e) - { - logger.LogVerbose(Resources.StatefulBranchProvider_BindingUpdated); - SafeClearSelectedBranchCache(); - - NotifySlCoreBranchChange(); - } - - private void SafeClearSelectedBranchCache() - { - using (asyncLock.Acquire()) - { - selectedBranch = null; - } - } - - private void OnPreSolutionBindingChanged(object sender, ActiveSolutionBindingEventArgs e) - { - logger.LogVerbose(Resources.StatefulBranchProvider_BindingChanged); - SafeClearSelectedBranchCache(); - - if (e.Configuration.Mode.IsInAConnectedMode()) - { - NotifySlCoreBranchChange(); - } - } - - private void NotifySlCoreBranchChange() => - threadHandling.RunOnBackgroundThread(() => - { - if (!serviceProvider.TryGetTransientService(out ISonarProjectBranchSlCoreService sonarProjectBranchSlCoreService)) - { - logger.LogVerbose(SLCoreStrings.ServiceProviderNotInitialized); - return; - } - if (activeConfigScopeTracker.Current == null) - { - return; - } - sonarProjectBranchSlCoreService.DidVcsRepositoryChange(new DidVcsRepositoryChangeParams(activeConfigScopeTracker.Current.Id)); - }).Forget(); - - public async Task GetServerBranchNameAsync(CancellationToken token) - { - using (await asyncLock.AcquireAsync()) - { - if (selectedBranch == null) - { - logger.LogVerbose(Resources.StatefulBranchProvider_CacheMiss); - - // Note: we're using null to indicate that a refresh is required. - // However, the serverBranchProvider will return null in some cases e.g. standalone mode, not a git repo. - // In these cases we expect the serverBranchProvider to return quickly so the impact of making unnecessary - // calls is not significant. - selectedBranch = await DoGetServerBranchNameAsync(token); - } - else - { - logger.LogVerbose(Resources.StatefulBranchProvider_CacheHit); - } - } - - logger.WriteLine(Resources.StatefulBranchProvider_ReturnValue, selectedBranch ?? Resources.NullBranchName); - return selectedBranch; - } - - private Task DoGetServerBranchNameAsync(CancellationToken token) => threadHandling.RunOnBackgroundThread(() => serverBranchProvider.GetServerBranchNameAsync(token)); - - #region IDisposable - - private void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - activeSolutionBoundTracker.PreSolutionBindingChanged -= OnPreSolutionBindingChanged; - activeSolutionBoundTracker.PreSolutionBindingUpdated -= OnPreSolutionBindingUpdated; - } - disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - #endregion IDisposable - } -} diff --git a/src/ConnectedMode/packages.lock.json b/src/ConnectedMode/packages.lock.json index 30aae1ca8d..f81f4a1b36 100644 --- a/src/ConnectedMode/packages.lock.json +++ b/src/ConnectedMode/packages.lock.json @@ -163,16 +163,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1398,8 +1388,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Core.UnitTests/packages.lock.json b/src/Core.UnitTests/packages.lock.json index 33a58cfe9b..3e3440eb3a 100644 --- a/src/Core.UnitTests/packages.lock.json +++ b/src/Core.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1308,8 +1298,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Core/Binding/IActiveSolutionBoundTracker.cs b/src/Core/Binding/IActiveSolutionBoundTracker.cs index 9181025b18..3fc5a894ca 100644 --- a/src/Core/Binding/IActiveSolutionBoundTracker.cs +++ b/src/Core/Binding/IActiveSolutionBoundTracker.cs @@ -57,17 +57,6 @@ public interface IActiveSolutionBoundTracker : IRequireInitialization /// opening/closing. /// event EventHandler SolutionBindingChanged; - - /// - /// Raised when an existing binding has been updated, and before the event - /// - event EventHandler PreSolutionBindingUpdated; - - /// - /// Raised when an existing binding has been updated i.e. the solution is still bound to the same - /// Sonar project (e.g. the user updated the binding via the Team Explorer) - /// - event EventHandler SolutionBindingUpdated; } public class ActiveSolutionBindingEventArgs : EventArgs diff --git a/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs b/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs index 2893ecb465..da8576715b 100644 --- a/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs +++ b/src/Core/ConfigurationScope/IActiveConfigScopeTracker.cs @@ -34,6 +34,8 @@ public interface IActiveConfigScopeTracker : IDisposable bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isReady); + bool TryUpdateMatchedBranchOnCurrentConfigScope(string id, string branch); + event EventHandler CurrentConfigurationScopeChanged; } @@ -51,7 +53,8 @@ public record ConfigurationScope( string SonarProjectId = null, string RootPath = null, string CommandsBaseDir = null, - bool IsReadyForAnalysis = false) + bool IsReadyForAnalysis = false, + string MatchedBranch = null) { public string Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id)); } diff --git a/src/Core/IServerBranchProvider.cs b/src/Core/IServerBranchProvider.cs deleted file mode 100644 index ed0f4ef2ef..0000000000 --- a/src/Core/IServerBranchProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace SonarLint.VisualStudio.Core -{ - public interface IServerBranchProvider - { - /// - /// Returns the Sonar server branch to use when requesting data - /// - /// The Sonar server branch name, - /// or the name of the Sonar server branch marked as "Main" if the branch cannot be determined, - /// or null if we are not in connected mode. - /// - /// - /// Only applies in connected mode. - /// - Task GetServerBranchNameAsync(CancellationToken token); - } - - /// - /// Stateful version of - /// - /// The implementation is a singleton that recalculates the Sonar branch name - /// automatically when necessary e.g. when a different solution is opened - /// - public interface IStatefulServerBranchProvider : IServerBranchProvider { } - -} diff --git a/src/Education.UnitTests/packages.lock.json b/src/Education.UnitTests/packages.lock.json index 89711facab..fc5956ddd8 100644 --- a/src/Education.UnitTests/packages.lock.json +++ b/src/Education.UnitTests/packages.lock.json @@ -122,16 +122,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1329,8 +1319,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Infrastructure.VS.UnitTests/packages.lock.json b/src/Infrastructure.VS.UnitTests/packages.lock.json index f3573e697a..34e16ffc97 100644 --- a/src/Infrastructure.VS.UnitTests/packages.lock.json +++ b/src/Infrastructure.VS.UnitTests/packages.lock.json @@ -141,16 +141,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1527,8 +1517,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Infrastructure.VS/packages.lock.json b/src/Infrastructure.VS/packages.lock.json index 48e18829fa..b6474f7051 100644 --- a/src/Infrastructure.VS/packages.lock.json +++ b/src/Infrastructure.VS/packages.lock.json @@ -161,16 +161,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1337,16 +1327,6 @@ "System.IO.Abstractions": "[9.0.4, )", "System.Threading.Channels": "[7.0.0, )" } - }, - "sonarqube.client": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "System.Net.Http": "[4.0.0, )" - } } } } diff --git a/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs b/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs index c3a4bcf321..079de04fb5 100644 --- a/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs +++ b/src/Integration.UnitTests/MefServices/ActiveSolutionBoundTrackerTests.cs @@ -45,7 +45,6 @@ public class ActiveSolutionBoundTrackerTests private TestLogger testLogger; private ISonarQubeService sonarQubeServiceMock; - private IBoundSolutionGitMonitor gitEventsMonitor; private IConfigScopeUpdater configScopeUpdater; private IInitializationProcessorFactory initializationProcessorFactory; private MockableInitializationProcessor createdInitializationProcessor; @@ -59,8 +58,7 @@ public void TestInitialize() { configProvider = Substitute.ForPartsOf(); activeSolutionTracker = Substitute.ForPartsOf(); - gitEventsMonitor = Substitute.For(); - initializationDependencies = [activeSolutionTracker, gitEventsMonitor]; + initializationDependencies = [activeSolutionTracker]; testLogger = new TestLogger(); vsMonitorMock = Substitute.For(); vsMonitorMock.GetCmdUIContextCookie(ref BoundSolutionUIContext.Guid, out Arg.Any()) @@ -86,7 +84,6 @@ public void MefCtor_CheckIsExported() => MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport()); @@ -109,9 +106,7 @@ public void ActiveSolutionBoundTracker_Initialisation_Unbound() threadHandling.RunOnUIThreadAsync(Arg.Any()); vsMonitorMock.GetCmdUIContextCookie(ref BoundSolutionUIContext.Guid, out Arg.Any()); _ = sonarQubeServiceMock.IsConnected; - gitEventsMonitor.Refresh(); activeSolutionTracker.ActiveSolutionChanged += Arg.Any>(); - gitEventsMonitor.HeadChanged += Arg.Any(); createdInitializationProcessor.InitializeAsync(); }); } @@ -134,9 +129,7 @@ public void ActiveSolutionBoundTracker_Initialisation_Bound() vsMonitorMock.GetCmdUIContextCookie(ref BoundSolutionUIContext.Guid, out Arg.Any()); _ = sonarQubeServiceMock.IsConnected; sonarQubeServiceMock.ConnectAsync(Arg.Any(), Arg.Any()); - gitEventsMonitor.Refresh(); activeSolutionTracker.ActiveSolutionChanged += Arg.Any>(); - gitEventsMonitor.HeadChanged += Arg.Any(); createdInitializationProcessor.InitializeAsync(); }); } @@ -151,8 +144,6 @@ public void DisposeBeforeInitialized_DisposeAndInitializeDoNothingWithEvents() activeSolutionTracker.DidNotReceive().ActiveSolutionChanged += Arg.Any>(); activeSolutionTracker.DidNotReceive().ActiveSolutionChanged -= Arg.Any>(); - gitEventsMonitor.DidNotReceive().HeadChanged += Arg.Any(); - gitEventsMonitor.DidNotReceive().HeadChanged -= Arg.Any(); } [TestMethod] @@ -162,7 +153,6 @@ public void Dispose_UnsubscribesFromEvents() testSubject.Dispose(); activeSolutionTracker.Received().ActiveSolutionChanged -= Arg.Any>(); - gitEventsMonitor.Received().HeadChanged -= Arg.Any(); } [TestMethod] @@ -207,7 +197,6 @@ public void ActiveSolutionBoundTracker_HandleBinding_EventsAreNotTriggeredBefore ConfigureSolutionBinding(boundSonarQubeProject); testSubject.HandleBindingChange(); - gitEventsMonitor.HeadChanged += Raise.Event(); // state is unmodified and events are not raised until initialized bindingChangedHandler.DidNotReceiveWithAnyArgs().Invoke(default, default); @@ -227,34 +216,6 @@ public void ActiveSolutionBoundTracker_HandleBinding_EventsAreNotTriggeredBefore testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Standalone); } - [TestMethod] - public void ActiveSolutionBoundTracker_GitHeadChanged_EventsAreNotTriggeredBeforeInitializationIsComplete() - { - ConfigureService(false); - ConfigureSolutionBinding(boundSonarQubeProject); - using var testSubject = CreateUninitializedTestSubject(out var barrier); - var bindingUpdatedEventHandler = Substitute.For(); - testSubject.SolutionBindingUpdated += bindingUpdatedEventHandler; - - testSubject.HandleBindingChange(); - activeSolutionTracker.SimulateActiveSolutionChanged(false, null); - activeSolutionTracker.SimulateActiveSolutionChanged(true, "name"); - gitEventsMonitor.HeadChanged += Raise.Event(); - - // state is unmodified and events are not raised until initialized - bindingUpdatedEventHandler.DidNotReceiveWithAnyArgs().Invoke(default, default); - configProvider.DidNotReceiveWithAnyArgs().GetConfiguration(); - testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Standalone); - - barrier.SetResult(1); - testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); - - // switch to connected mode to test binding updated event - testSubject.CurrentConfiguration.Mode.Should().Be(SonarLintMode.Connected); - gitEventsMonitor.HeadChanged += Raise.Event(); - bindingUpdatedEventHandler.ReceivedWithAnyArgs().Invoke(default, default); - } - [TestMethod] public void ActiveSolutionBoundTracker_WhenConnectionCannotBeEstablished_ReportAsStandalone() { @@ -593,61 +554,6 @@ public void HandleBindingChange_WhenProjectChanged_RaisedChangedEvents() eventCounter.SolutionBindingChangedCount.Should().Be(1); } - [TestMethod] - public void GitRepoUpdated_SolutionBindingUpdatedEventsRaised() - { - // Arrange - ConfigureService(false); - ConfigureSolutionBinding(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://foo")))); - using var testSubject = CreateAndInitializeTestSubject(); - var eventCounter = new EventCounter(testSubject); - - // Act - gitEventsMonitor.HeadChanged += Raise.Event(); - - // Assert - eventCounter.PreSolutionBindingUpdatedCount.Should().Be(1); - eventCounter.SolutionBindingUpdatedCount.Should().Be(1); - eventCounter.PreSolutionBindingChangedCount.Should().Be(0); - eventCounter.SolutionBindingChangedCount.Should().Be(0); - - eventCounter.RaisedEventNames.Should().HaveCount(2); - eventCounter.RaisedEventNames[0].Should().Be(nameof(testSubject.PreSolutionBindingUpdated)); - eventCounter.RaisedEventNames[1].Should().Be(nameof(testSubject.SolutionBindingUpdated)); - } - - [TestMethod] - public void GitRepoUpdated_UnBoundProject_SolutionBingingUpdatedNotInvoked() - { - // Arrange - using var testSubject = CreateAndInitializeTestSubject(); - var eventCounter = new EventCounter(testSubject); - - // Act - ConfigureSolutionBinding(null); - gitEventsMonitor.HeadChanged += Raise.Event(); - - // Assert - eventCounter.RaisedEventNames.Should().HaveCount(0); - } - - [TestMethod] - public void ActiveSolutionChanged_GitEventsMonitorRefreshInvoked() - { - ConfigureService(true); - ConfigureSolutionBinding(new BoundServerProject("solution", "projectKey", new ServerConnection.SonarQube(new Uri("http://foo")))); - - // Arrange - using var testSubject = CreateAndInitializeTestSubject(); - gitEventsMonitor.ClearReceivedCalls(); // invoked during initialization - - // Act - activeSolutionTracker.SimulateActiveSolutionChanged(true, "solution"); - - // Assert - gitEventsMonitor.Received(1).Refresh(); - } - private ActiveSolutionBoundTracker CreateUninitializedTestSubject(out TaskCompletionSource barrier) { var tcs = barrier = new(); @@ -656,7 +562,7 @@ private ActiveSolutionBoundTracker CreateUninitializedTestSubject(out TaskComple createdInitializationProcessor = processor; MockableInitializationProcessor.ConfigureWithWait(processor, tcs); }); - return new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, gitEventsMonitor, configProvider, sonarQubeServiceMock, + return new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, configProvider, sonarQubeServiceMock, initializationProcessorFactory); } @@ -664,7 +570,7 @@ private ActiveSolutionBoundTracker CreateAndInitializeTestSubject() { initializationProcessorFactory = MockableInitializationProcessor.CreateFactory(threadHandling, new TestLogger(), processor => createdInitializationProcessor = processor); - var testSubject = new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, gitEventsMonitor, configProvider, sonarQubeServiceMock, + var testSubject = new ActiveSolutionBoundTracker(serviceProvider, activeSolutionTracker, configScopeUpdater, testLogger, configProvider, sonarQubeServiceMock, initializationProcessorFactory); testSubject.InitializationProcessor.InitializeAsync().GetAwaiter().GetResult(); return testSubject; @@ -732,18 +638,6 @@ public EventCounter(IActiveSolutionBoundTracker tracker) SolutionBindingChangedCount++; raisedEventNames.Add(nameof(IActiveSolutionBoundTracker.SolutionBindingChanged)); }; - - tracker.PreSolutionBindingUpdated += (_, _) => - { - PreSolutionBindingUpdatedCount++; - raisedEventNames.Add(nameof(IActiveSolutionBoundTracker.PreSolutionBindingUpdated)); - }; - - tracker.SolutionBindingUpdated += (_, _) => - { - SolutionBindingUpdatedCount++; - raisedEventNames.Add(nameof(IActiveSolutionBoundTracker.SolutionBindingUpdated)); - }; } } } diff --git a/src/Integration.UnitTests/packages.lock.json b/src/Integration.UnitTests/packages.lock.json index 76b6b1dc02..fdb8191384 100644 --- a/src/Integration.UnitTests/packages.lock.json +++ b/src/Integration.UnitTests/packages.lock.json @@ -224,16 +224,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1315,8 +1305,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Integration.Vsix.UnitTests/packages.lock.json b/src/Integration.Vsix.UnitTests/packages.lock.json index 1a82191461..afe6a8db8b 100644 --- a/src/Integration.Vsix.UnitTests/packages.lock.json +++ b/src/Integration.Vsix.UnitTests/packages.lock.json @@ -162,16 +162,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1507,8 +1497,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt index 867e7faec7..78a92e13b7 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2025-10-14T14:04:05.5092281Z +# Report date/time: 2025-10-14T14:24:04.3472038Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -338,13 +338,12 @@ Assembly: 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c Relative path: 'SonarQube.Client.dll' Referenced assemblies: -- 'Google.Protobuf, Version=3.6.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -# Number of references: 7 +# Number of references: 6 ... diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt index 2fa27ec438..b84e1a929f 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2025-10-14T14:04:05.5092281Z +# Report date/time: 2025-10-14T14:24:04.3472038Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -338,13 +338,12 @@ Assembly: 'SonarQube.Client, Version=8.30.0.0, Culture=neutral, PublicKeyToken=n Relative path: 'SonarQube.Client.dll' Referenced assemblies: -- 'Google.Protobuf, Version=3.6.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' - 'SonarLint.VisualStudio.Core, Version=8.30.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -# Number of references: 7 +# Number of references: 6 ... diff --git a/src/Integration.Vsix/app.config b/src/Integration.Vsix/app.config index a165d7d6f6..97fd28e340 100644 --- a/src/Integration.Vsix/app.config +++ b/src/Integration.Vsix/app.config @@ -62,10 +62,6 @@ - - - - diff --git a/src/Integration.Vsix/packages.lock.json b/src/Integration.Vsix/packages.lock.json index 9a607d6dd1..6828464143 100644 --- a/src/Integration.Vsix/packages.lock.json +++ b/src/Integration.Vsix/packages.lock.json @@ -190,16 +190,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1532,18 +1522,6 @@ "SonarLint.VisualStudio.IssueVisualization": "[1.0.0, )" } }, - "SonarLint.VisualStudio.Roslyn.Suppressions": { - "type": "Project", - "dependencies": { - "Microsoft.VisualStudio.Sdk": "[17.0.31902.203, )", - "Newtonsoft.Json": "[13.0.3, )", - "SonarLint.VisualStudio.ConnectedMode": "[1.0.0, )", - "SonarLint.VisualStudio.Core": "[1.0.0, )", - "SonarLint.VisualStudio.Infrastructure.VS": "[1.0.0, )", - "SonarQube.Client": "[1.0.0, )", - "System.IO.Abstractions": "[9.0.4, )" - } - }, "SonarLint.VisualStudio.RoslynAnalyzerServer": { "type": "Project", "dependencies": { @@ -1569,8 +1547,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/Integration/MefServices/ActiveSolutionBoundTracker.cs b/src/Integration/MefServices/ActiveSolutionBoundTracker.cs index 9544fe3599..49a056ab78 100644 --- a/src/Integration/MefServices/ActiveSolutionBoundTracker.cs +++ b/src/Integration/MefServices/ActiveSolutionBoundTracker.cs @@ -50,7 +50,6 @@ internal sealed class ActiveSolutionBoundTracker : IActiveSolutionBoundTracker, private readonly IActiveSolutionTracker solutionTracker; private readonly IConfigurationProvider configurationProvider; private readonly ISonarQubeService sonarQubeService; - private readonly IBoundSolutionGitMonitor gitEventsMonitor; private readonly IConfigScopeUpdater configScopeUpdater; private readonly ILogger logger; private IVsMonitorSelection vsMonitorSelection; @@ -59,8 +58,6 @@ internal sealed class ActiveSolutionBoundTracker : IActiveSolutionBoundTracker, public event EventHandler PreSolutionBindingChanged; public event EventHandler SolutionBindingChanged; - public event EventHandler PreSolutionBindingUpdated; - public event EventHandler SolutionBindingUpdated; public BindingConfiguration CurrentConfiguration { get; private set; } [ImportingConstructor] @@ -69,20 +66,18 @@ public ActiveSolutionBoundTracker( IActiveSolutionTracker activeSolutionTracker, IConfigScopeUpdater configScopeUpdater, ILogger logger, - IBoundSolutionGitMonitor gitEventsMonitor, IConfigurationProvider configurationProvider, ISonarQubeService sonarQubeService, IInitializationProcessorFactory initializationProcessorFactory) { this.serviceProvider = serviceProvider; solutionTracker = activeSolutionTracker; - this.gitEventsMonitor = gitEventsMonitor; this.logger = logger; this.configurationProvider = configurationProvider; this.sonarQubeService = sonarQubeService; this.configScopeUpdater = configScopeUpdater; InitializationProcessor = initializationProcessorFactory.Create( - [solutionTracker, this.gitEventsMonitor], + [solutionTracker], InitializeInternalAsync); CurrentConfiguration = BindingConfiguration.Standalone; @@ -107,7 +102,6 @@ await threadHandling.RunOnUIThreadAsync(() => return; } solutionTracker.ActiveSolutionChanged += OnActiveSolutionChanged; - gitEventsMonitor.HeadChanged += GitEventsMonitor_HeadChanged; } public void HandleBindingChange() @@ -121,15 +115,6 @@ public void HandleBindingChange() RaiseAnalyzersChangedIfBindingChanged(newBindingConfiguration); } - private void GitEventsMonitor_HeadChanged(object sender, EventArgs e) - { - if (CurrentConfiguration.Mode.IsInAConnectedMode()) - { - PreSolutionBindingUpdated?.Invoke(this, EventArgs.Empty); - SolutionBindingUpdated?.Invoke(this, EventArgs.Empty); - } - } - private async void OnActiveSolutionChanged(object sender, ActiveSolutionChangedEventArgs args) { // An exception here will crash VS @@ -149,8 +134,6 @@ private async Task HandleActiveSolutionChangeAsync() var connectionUpdatedSuccessfully = await UpdateConnectionAsync(newBindingConfiguration); - gitEventsMonitor.Refresh(); - RaiseAnalyzersChangedIfBindingChanged(connectionUpdatedSuccessfully ? newBindingConfiguration : BindingConfiguration.Standalone); } @@ -220,7 +203,6 @@ public void Dispose() if (InitializationProcessor.IsFinalized) { solutionTracker.ActiveSolutionChanged -= OnActiveSolutionChanged; - gitEventsMonitor.HeadChanged -= GitEventsMonitor_HeadChanged; } disposed = true; } diff --git a/src/Integration/packages.lock.json b/src/Integration/packages.lock.json index a541b4f223..7ad708a3ef 100644 --- a/src/Integration/packages.lock.json +++ b/src/Integration/packages.lock.json @@ -160,16 +160,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1199,8 +1189,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz.Security.UnitTests/packages.lock.json b/src/IssueViz.Security.UnitTests/packages.lock.json index 57c42a196f..a20214c256 100644 --- a/src/IssueViz.Security.UnitTests/packages.lock.json +++ b/src/IssueViz.Security.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1341,8 +1331,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz.Security/packages.lock.json b/src/IssueViz.Security/packages.lock.json index 815fb79e24..c57fd83523 100644 --- a/src/IssueViz.Security/packages.lock.json +++ b/src/IssueViz.Security/packages.lock.json @@ -137,16 +137,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1199,8 +1189,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz.UnitTests/packages.lock.json b/src/IssueViz.UnitTests/packages.lock.json index 33a58cfe9b..3e3440eb3a 100644 --- a/src/IssueViz.UnitTests/packages.lock.json +++ b/src/IssueViz.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1308,8 +1298,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/IssueViz/packages.lock.json b/src/IssueViz/packages.lock.json index ebe8746688..c01fcaa900 100644 --- a/src/IssueViz/packages.lock.json +++ b/src/IssueViz/packages.lock.json @@ -118,16 +118,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1135,8 +1125,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json index 7acc230628..ef108b8da2 100644 --- a/src/RoslynAnalyzerServer.UnitTests/packages.lock.json +++ b/src/RoslynAnalyzerServer.UnitTests/packages.lock.json @@ -148,16 +148,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.2.0", @@ -1425,8 +1415,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.IntegrationTests/packages.lock.json b/src/SLCore.IntegrationTests/packages.lock.json index cd6914598b..0da00f606f 100644 --- a/src/SLCore.IntegrationTests/packages.lock.json +++ b/src/SLCore.IntegrationTests/packages.lock.json @@ -116,16 +116,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1401,8 +1391,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs b/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs index b178509771..5d6a0ce25b 100644 --- a/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/BranchListenerTests.cs @@ -19,6 +19,7 @@ */ using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -27,54 +28,91 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.UnitTests [TestClass] public class BranchListenerTests { - private IStatefulServerBranchProvider statefulServerBranchProvider; + private IServerBranchProvider serverBranchProvider; + private IActiveConfigScopeTracker activeConfigScopeTracker; + private TestLogger logger; private BranchListener testSubject; [TestInitialize] public void TestInitialize() { - statefulServerBranchProvider = Substitute.For(); - testSubject = new BranchListener(statefulServerBranchProvider); + serverBranchProvider = Substitute.For(); + activeConfigScopeTracker = Substitute.For(); + logger = Substitute.ForPartsOf(); + + testSubject = new BranchListener(serverBranchProvider, activeConfigScopeTracker, logger); } [TestMethod] - public void MefCtor_CheckIsExported() - { + public void MefCtor_CheckIsExported() => MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport()); - } + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); [TestMethod] - public void Mef_CheckIsSingleton() + public void Mef_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent(); + + [TestMethod] + public async Task MatchSonarProjectBranchAsync_ConfigurationScopeIdDoesNotMatch_ReturnsNull() { - MefTestHelpers.CheckIsSingletonMefComponent(); + var configScope = new ConfigurationScope("different-id"); + activeConfigScopeTracker.Current.Returns(configScope); + var parameters = new MatchSonarProjectBranchParams("config-id", "branch1", ["branch1", "branch2"]); + + var result = await testSubject.MatchSonarProjectBranchAsync(parameters); + + result.matchedSonarBranch.Should().BeNull(); + serverBranchProvider.DidNotReceiveWithAnyArgs().GetServerBranchName(default); } [TestMethod] - public async Task MatchSonarProjectBranch_ReturnsCalculatedBranch() + public async Task MatchSonarProjectBranchAsync_ConfigurationScopeIdMatches_ReturnsMatchingBranch() { - const string checkedOutBranch = "branch2"; + const string configId = "config-id"; + var configScope = new ConfigurationScope(configId); + activeConfigScopeTracker.Current.Returns(configScope); + var parameters = new MatchSonarProjectBranchParams(configId, "main", ["branch1", "branch2", "main"]); + const string expectedBranch = "branch1"; + serverBranchProvider.GetServerBranchName(Arg.Any>()).Returns(expectedBranch); + + var result = await testSubject.MatchSonarProjectBranchAsync(parameters); - statefulServerBranchProvider.GetServerBranchNameAsync(CancellationToken.None).Returns(checkedOutBranch); + result.matchedSonarBranch.Should().Be(expectedBranch); + serverBranchProvider.Received(1).GetServerBranchName(Arg.Is>(list => + list.Count == 3 && + list.Any(b => b.Name == "branch1" && !b.IsMain) && + list.Any(b => b.Name == "branch2" && !b.IsMain) && + list.Any(b => b.Name == "main" && b.IsMain))); + } - var param = new MatchSonarProjectBranchParams("scopeId", - "mainBranch", - ["branch1", "branch2", "mainBranch"]); + [TestMethod] + public async Task DidChangeMatchedSonarProjectBranchAsync_UpdatesConfigScope_WhenIdMatches() + { + const string configId = "config-id"; + const string branchName = "new-branch"; + activeConfigScopeTracker.TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName).Returns(true); + var parameters = new DidChangeMatchedSonarProjectBranchParams(configId, branchName); - var result = await testSubject.MatchSonarProjectBranchAsync(param); + await testSubject.DidChangeMatchedSonarProjectBranchAsync(parameters); - result.matchedSonarBranch.Should().Be(checkedOutBranch); + activeConfigScopeTracker.Received(1).TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName); } [TestMethod] - [DataRow(null)] - [DataRow(5)] - [DataRow("something")] - public void DidChangeMatchedSonarProjectBranch_ReturnsTaskCompleted(object parameter) + public async Task DidChangeMatchedSonarProjectBranchAsync_LogsError_WhenIdDoesNotMatch() { - var result = testSubject.DidChangeMatchedSonarProjectBranchAsync(parameter); + const string configId = "config-id"; + const string branchName = "new-branch"; + activeConfigScopeTracker.TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName).Returns(false); + var configScope = new ConfigurationScope("different-id"); + activeConfigScopeTracker.Current.Returns(configScope); + var parameters = new DidChangeMatchedSonarProjectBranchParams(configId, branchName); + + await testSubject.DidChangeMatchedSonarProjectBranchAsync(parameters); - result.Should().Be(Task.CompletedTask); + activeConfigScopeTracker.Received(1).TryUpdateMatchedBranchOnCurrentConfigScope(configId, branchName); + logger.AssertPartialOutputStringExists(string.Format(SLCoreStrings.ConfigurationScopeMismatch, configId, configScope.Id)); } } } diff --git a/src/SLCore.Listeners.UnitTests/packages.lock.json b/src/SLCore.Listeners.UnitTests/packages.lock.json index a8bc9231ee..922e1126b4 100644 --- a/src/SLCore.Listeners.UnitTests/packages.lock.json +++ b/src/SLCore.Listeners.UnitTests/packages.lock.json @@ -132,16 +132,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.1.194", @@ -1346,8 +1336,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.Listeners/Implementation/BranchListener.cs b/src/SLCore.Listeners/Implementation/BranchListener.cs index fc85af3e27..615d63618c 100644 --- a/src/SLCore.Listeners/Implementation/BranchListener.cs +++ b/src/SLCore.Listeners/Implementation/BranchListener.cs @@ -20,6 +20,7 @@ using System.ComponentModel.Composition; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.SLCore.Core; using SonarLint.VisualStudio.SLCore.Listener.Branch; @@ -28,24 +29,38 @@ namespace SonarLint.VisualStudio.SLCore.Listeners.Implementation [Export(typeof(ISLCoreListener))] [PartCreationPolicy(CreationPolicy.Shared)] [method: ImportingConstructor] - public class BranchListener(IStatefulServerBranchProvider statefulServerBranchProvider) : IBranchListener + public class BranchListener(IServerBranchProvider serverBranchProvider, IActiveConfigScopeTracker activeConfigScopeTracker, ILogger log) : IBranchListener { + private readonly ILogger log = log.ForContext(SLCoreStrings.SLCoreAnalysisConfigurationLogContext, SLCoreStrings.ConnectedMode_LogContext).ForVerboseContext(nameof(BranchListener)); + /// /// Request to calculate the matching branch between the local project and the sonar server /// - public async Task MatchSonarProjectBranchAsync(MatchSonarProjectBranchParams parameters) + public Task MatchSonarProjectBranchAsync(MatchSonarProjectBranchParams parameters) { - var matchingBranchName = await statefulServerBranchProvider.GetServerBranchNameAsync(CancellationToken.None); - return new MatchSonarProjectBranchResponse(matchingBranchName); + if (activeConfigScopeTracker.Current.Id is var currentId && currentId != parameters.configurationScopeId) + { + log.WriteLine(SLCoreStrings.ConfigurationScopeMismatch, parameters.configurationScopeId, currentId); + return Task.FromResult(new MatchSonarProjectBranchResponse(null)); + } + + var matchingBranchName = serverBranchProvider.GetServerBranchName(parameters.allSonarBranchesNames + .Select(x => new RemoteBranch(x, x == parameters.mainSonarBranchName)) + .ToList()); + + return Task.FromResult(new MatchSonarProjectBranchResponse(matchingBranchName)); } /// - /// Stub method for compability with SLCore. TODO https://github.com/SonarSource/sonarlint-visualstudio/issues/5401 + /// Handles calculated branch notification from SLCore /// - /// Parameter's here for compability we discard it - /// This will be implemented properly in the future when needed but features we support does not need branch awareness for now - public Task DidChangeMatchedSonarProjectBranchAsync(object parameters) + public Task DidChangeMatchedSonarProjectBranchAsync(DidChangeMatchedSonarProjectBranchParams parameters) { + if (!activeConfigScopeTracker.TryUpdateMatchedBranchOnCurrentConfigScope(parameters.configScopeId, parameters.newMatchedBranchName)) + { + log.WriteLine(SLCoreStrings.ConfigurationScopeMismatch, parameters.configScopeId, activeConfigScopeTracker.Current.Id); + } + return Task.CompletedTask; } } diff --git a/src/SLCore.Listeners/packages.lock.json b/src/SLCore.Listeners/packages.lock.json index 4cfe80fb1f..deb2b2e456 100644 --- a/src/SLCore.Listeners/packages.lock.json +++ b/src/SLCore.Listeners/packages.lock.json @@ -47,16 +47,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1213,8 +1203,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs b/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs index 0330a21925..d6ac363bf2 100644 --- a/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs +++ b/src/SLCore.UnitTests/State/ActiveConfigScopeTrackerTests.cs @@ -163,6 +163,41 @@ public void TryUpdateAnalysisReadinessOnCurrentConfigScope_ConfigScopeDifferent_ VerifyCurrentConfigurationScopeChangedNotRaised(); } + [TestMethod] + public void TryUpdateMatchedBranchOnCurrentConfigScope_ConfigScopeSame_Updates() + { + const string configScopeId = "myid"; + const string connectionId = "connectionid"; + const string sonarProjectKey = "projectkey"; + const string root = "root"; + const string baseDir = "basedir"; + const string branch = "branch"; + testSubject.CurrentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, baseDir, true, branch); + + var result = testSubject.TryUpdateMatchedBranchOnCurrentConfigScope(configScopeId, branch); + + result.Should().BeTrue(); + testSubject.CurrentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, baseDir, true, branch)); + VerifyCurrentConfigurationScopeChangedRaised(false); + logger.AssertPartialOutputStringExists(string.Format(SLCoreStrings.ConfigScope_UpdatedSonarBranch, configScopeId, branch)); + } + + [TestMethod] + public void TryUpdateMatchedBranchOnCurrentConfigScope_ConfigScopeDifferent_DoesNotUpdate() + { + const string configScopeId = "myid"; + const string connectionId = "connectionid"; + const string sonarProjectKey = "projectkey"; + const string root = "root"; + testSubject.CurrentConfigScope = new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root); + + var result = testSubject.TryUpdateMatchedBranchOnCurrentConfigScope("some other id", "branch"); + + result.Should().BeFalse(); + testSubject.CurrentConfigScope.Should().BeEquivalentTo(new ConfigurationScope(configScopeId, connectionId, sonarProjectKey, root, MatchedBranch: null)); + VerifyCurrentConfigurationScopeChangedNotRaised(); + } + [TestMethod] public void SetCurrentConfigScope_SetsBoundScope() { diff --git a/src/SLCore.UnitTests/packages.lock.json b/src/SLCore.UnitTests/packages.lock.json index 33a58cfe9b..3e3440eb3a 100644 --- a/src/SLCore.UnitTests/packages.lock.json +++ b/src/SLCore.UnitTests/packages.lock.json @@ -111,16 +111,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1308,8 +1298,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SonarQube.Client/Api/IGetProjectBranchesRequest.cs b/src/SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs similarity index 72% rename from src/SonarQube.Client/Api/IGetProjectBranchesRequest.cs rename to src/SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs index 04f8bc08f3..23f96d44ed 100644 --- a/src/SonarQube.Client/Api/IGetProjectBranchesRequest.cs +++ b/src/SLCore/Listener/Branch/DidChangeMatchedSonarProjectBranchParams.cs @@ -18,16 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using SonarQube.Client.Models; -using SonarQube.Client.Requests; +namespace SonarLint.VisualStudio.SLCore.Listener.Branch; -namespace SonarQube.Client.Api -{ - /// - /// Returns branch information for the specified project - /// - public interface IGetProjectBranchesRequest : IRequest - { - string ProjectKey { get; set; } - } -} +public record DidChangeMatchedSonarProjectBranchParams(string configScopeId, string newMatchedBranchName); diff --git a/src/SLCore/Listener/Branch/IBranchListener.cs b/src/SLCore/Listener/Branch/IBranchListener.cs index 837996b28d..7175971a2a 100644 --- a/src/SLCore/Listener/Branch/IBranchListener.cs +++ b/src/SLCore/Listener/Branch/IBranchListener.cs @@ -36,5 +36,5 @@ public interface IBranchListener : ISLCoreListener /// /// Parameter's here for compability we discard it /// This will be implemented properly in the future when needed but features we support does not need branch awareness for now - Task DidChangeMatchedSonarProjectBranchAsync(object parameters); + Task DidChangeMatchedSonarProjectBranchAsync(DidChangeMatchedSonarProjectBranchParams parameters); } diff --git a/src/SonarQube.Client/Models/SonarQubeProjectBranch.cs b/src/SLCore/Listener/Branch/IServerBranchProvider.cs similarity index 58% rename from src/SonarQube.Client/Models/SonarQubeProjectBranch.cs rename to src/SLCore/Listener/Branch/IServerBranchProvider.cs index 30c1711fcf..3cfe9751ee 100644 --- a/src/SonarQube.Client/Models/SonarQubeProjectBranch.cs +++ b/src/SLCore/Listener/Branch/IServerBranchProvider.cs @@ -18,23 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; +namespace SonarLint.VisualStudio.SLCore.Listener.Branch; -namespace SonarQube.Client.Models -{ - public class SonarQubeProjectBranch - { - public string Name { get; } - public bool IsMain { get; } - public DateTimeOffset LastAnalysisTimestamp { get; } - public string Type { get; set; } +public record struct RemoteBranch(string Name, bool IsMain); - public SonarQubeProjectBranch(string name, bool isMain, DateTimeOffset analysisDate, string type) - { - Name = name; - IsMain = isMain; - LastAnalysisTimestamp = analysisDate; - Type = type; - } - } +public interface IServerBranchProvider +{ + /// + /// Returns the Sonar server branch to use when requesting data + /// + /// The Sonar server branch name, + /// or the name of the Sonar server branch marked as "Main" if the branch cannot be determined, + /// or null if we are not in connected mode. + /// + /// + /// Only applies in connected mode. + /// + string? GetServerBranchName(List branches); } diff --git a/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs b/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs index fb04eb0d4f..7017a789f3 100644 --- a/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs +++ b/src/SLCore/Listener/Branch/MatchSonarProjectBranchResponse.cs @@ -20,4 +20,4 @@ namespace SonarLint.VisualStudio.SLCore.Listener.Branch; -public record MatchSonarProjectBranchResponse(string matchedSonarBranch); +public record MatchSonarProjectBranchResponse(string? matchedSonarBranch); diff --git a/src/SLCore/SLCoreStrings.Designer.cs b/src/SLCore/SLCoreStrings.Designer.cs index 08014bdeab..19e6f73bf3 100644 --- a/src/SLCore/SLCoreStrings.Designer.cs +++ b/src/SLCore/SLCoreStrings.Designer.cs @@ -230,6 +230,15 @@ public static string ConfigScope_UpdatedFileSystem { } } + /// + /// Looks up a localized string similar to Updated Sonar branch for Configuration Scope {0}: {1}. + /// + public static string ConfigScope_UpdatedSonarBranch { + get { + return ResourceManager.GetString("ConfigScope_UpdatedSonarBranch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Configuration scope conflict. /// diff --git a/src/SLCore/SLCoreStrings.resx b/src/SLCore/SLCoreStrings.resx index 41c4a82bbc..e6557414c9 100644 --- a/src/SLCore/SLCoreStrings.resx +++ b/src/SLCore/SLCoreStrings.resx @@ -294,4 +294,7 @@ Updated analysis readiness for Configuration Scope {0}: {1} + + Updated Sonar branch for Configuration Scope {0}: {1} + \ No newline at end of file diff --git a/src/SLCore/State/ActiveConfigScopeTracker.cs b/src/SLCore/State/ActiveConfigScopeTracker.cs index 6b2d0d352e..9be1b7aa23 100644 --- a/src/SLCore/State/ActiveConfigScopeTracker.cs +++ b/src/SLCore/State/ActiveConfigScopeTracker.cs @@ -137,24 +137,28 @@ public void RemoveCurrentConfigScope() OnCurrentConfigurationScopeChanged(true); } - public bool TryUpdateRootOnCurrentConfigScope(string id, string root, string commandsBaseDir) - { - using (asyncLock.Acquire()) + public bool TryUpdateRootOnCurrentConfigScope(string? id, string root, string commandsBaseDir) => + UpdateCurrentScope(id, () => { - if (id is null || CurrentConfigScope?.Id != id) - { - return false; - } - - CurrentConfigScope = CurrentConfigScope with { RootPath = root, CommandsBaseDir = commandsBaseDir }; + CurrentConfigScope = CurrentConfigScope! with { RootPath = root, CommandsBaseDir = commandsBaseDir }; logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedFileSystem, id, root, commandsBaseDir); - LogConfigurationScopeChangedUnsafe(); - } - OnCurrentConfigurationScopeChanged(false); - return true; - } + }); - public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isReady) + public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string? id, bool isReady) => + UpdateCurrentScope(id, () => + { + CurrentConfigScope = CurrentConfigScope! with { IsReadyForAnalysis = isReady}; + logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedAnalysisReadiness, id, isReady); + }); + + public bool TryUpdateMatchedBranchOnCurrentConfigScope(string? id, string branch) => + UpdateCurrentScope(id, () => + { + CurrentConfigScope = CurrentConfigScope! with { MatchedBranch = branch }; + logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedSonarBranch, id, branch); + }); + + private bool UpdateCurrentScope(string? id, Action update) { using (asyncLock.Acquire()) { @@ -163,8 +167,7 @@ public bool TryUpdateAnalysisReadinessOnCurrentConfigScope(string id, bool isRea return false; } - CurrentConfigScope = CurrentConfigScope with { IsReadyForAnalysis = isReady}; - logger.WriteLine(SLCoreStrings.ConfigScope_UpdatedAnalysisReadiness, id, isReady); + update.Invoke(); LogConfigurationScopeChangedUnsafe(); } OnCurrentConfigurationScopeChanged(false); diff --git a/src/SonarQube.Client/Messages/Protobuf/constants.proto b/src/SLCore/State/ISlCoreGitChangeNotifier.cs similarity index 77% rename from src/SonarQube.Client/Messages/Protobuf/constants.proto rename to src/SLCore/State/ISlCoreGitChangeNotifier.cs index f70c263beb..03aa4dc6e2 100644 --- a/src/SonarQube.Client/Messages/Protobuf/constants.proto +++ b/src/SLCore/State/ISlCoreGitChangeNotifier.cs @@ -1,4 +1,4 @@ -/* +/* * SonarLint for Visual Studio * Copyright (C) 2016-2025 SonarSource SA * mailto:info AT sonarsource DOT com @@ -18,18 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -syntax = "proto3"; +using SonarLint.VisualStudio.Core.Initialization; -package SonarQube.Client.Messages; +namespace SonarLint.VisualStudio.SLCore.State; -option java_package = "org.sonar.scanner.protocol"; -option optimize_for = SPEED; - -enum Severity { - UNSET_SEVERITY = 0; - INFO = 1; - MINOR = 2; - MAJOR = 3; - CRITICAL = 4; - BLOCKER = 5; -} \ No newline at end of file +public interface ISlCoreGitChangeNotifier : IRequireInitialization, IDisposable; diff --git a/src/SonarQube.Client.Tests/IssuesProtobufResponse b/src/SonarQube.Client.Tests/IssuesProtobufResponse deleted file mode 100644 index b2582e9d8c3b653da38877141a9a36b7b4f98a99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 720 zcmb`FyH3L}6o$DDz=oKZtc;XaanfAci4=-JNNGh8NNkRM(nK_|u^mcR9)f4#5qJdN zfB_x?rywp80|Sy}|2ndc{C&r-i|bp_c@-fW4iqTTuC1ra{* zZQML`z5Qkzhha19xIxqH;V87jzUzC=_WDrru_Q%{Dr zj3I(7A-M*sfmw+1QfXOH%_TBRXMilFiO#8ou3$Fhk|E9KbvhwRAqM7@<^nHUd&@@) zmn(x|KTeQMQi2l?e1{O1rX}@*(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); } [TestMethod] @@ -90,7 +87,6 @@ public void ConfigureSonarCloud_CheckAllRequestsImplemented() testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); testSubject.Create(serverInfo).Should().NotBeNull(); - testSubject.Create(serverInfo).Should().NotBeNull(); } private static void DumpDebugMessages(TestLogger logger) diff --git a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj index c36ca4054f..69e42f4038 100644 --- a/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj +++ b/src/SonarQube.Client.Tests/SonarQube.Client.Tests.csproj @@ -12,10 +12,6 @@ true - - - - @@ -24,12 +20,6 @@ - - - Always - - - diff --git a/src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs b/src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs deleted file mode 100644 index 845176a0bb..0000000000 --- a/src/SonarQube.Client.Tests/SonarQubeService_GetProjectBranchesAsync.cs +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Net; -using System.Net.Http; -using SonarQube.Client.Models; - -namespace SonarQube.Client.Tests -{ - [TestClass] - public class SonarQubeService_GetProjectBranchesAsync : SonarQubeService_TestBase - { - [TestMethod] - public async Task Get_V6_6() - { - await ConnectToSonarQube("6.6.0.0"); - - const string response = @"{ - ""branches"": [ - { - ""name"": ""feature/foo"", - ""isMain"": false, - ""type"": ""SHORT"", - ""status"": { - ""qualityGateStatus"": ""OK"" - }, - ""analysisDate"": ""2017-04-03T13:37:00+0100"", - ""excludedFromPurge"": false - }, - { - ""name"": ""master"", - ""isMain"": true, - ""type"": ""LONG"", - ""status"": { - ""qualityGateStatus"": ""ERROR"" - }, - ""analysisDate"": ""2018-12-01T01:15:42-0400"", - ""excludedFromPurge"": true - } - ] -}"; - SetupRequest("api/project_branches/list?project=my_project", - response); - - var result = await service.GetProjectBranchesAsync("my_project", CancellationToken.None); - - httpClientHandler.VerifyAll(); - - result.Should().HaveCount(2); - result[0].Name.Should().Be("feature/foo"); - result[0].IsMain.Should().Be(false); - result[0].Type.Should().Be("SHORT"); - result[0].LastAnalysisTimestamp.Should().Be(new DateTimeOffset(2017, 4, 3, 13, 37, 0, TimeSpan.FromHours(1))); - - result[1].Name.Should().Be("master"); - result[1].IsMain.Should().Be(true); - result[1].Type.Should().Be("LONG"); - result[1].LastAnalysisTimestamp.Should().Be(new DateTimeOffset(2018, 12, 1, 1, 15, 42, TimeSpan.FromHours(-4))); - } - - [TestMethod] - public async Task Get_NotFound() - { - await ConnectToSonarQube("6.6.0.0"); - - SetupRequest("api/project_branches/list?project=missing", "", HttpStatusCode.NotFound); - - Func>> func = async () => - await service.GetProjectBranchesAsync("missing", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("Response status code does not indicate success: 404 (Not Found)."); - - httpClientHandler.VerifyAll(); - } - - [TestMethod] - public void Get_NotConnected() - { - // No calls to Connect - // No need to setup request, the operation should fail - - Func>> func = async () => - await service.GetProjectBranchesAsync("any", CancellationToken.None); - - func.Should().ThrowExactly().And - .Message.Should().Be("This operation expects the service to be connected."); - - logger.ErrorMessages.Should().Contain("The service is expected to be connected."); - } - } -} diff --git a/src/SonarQube.Client.Tests/TestResources/IssuesProtobufResponse b/src/SonarQube.Client.Tests/TestResources/IssuesProtobufResponse deleted file mode 100644 index b2582e9d8c3b653da38877141a9a36b7b4f98a99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 720 zcmb`FyH3L}6o$DDz=oKZtc;XaanfAci4=-JNNGh8NNkRM(nK_|u^mcR9)f4#5qJdN zfB_x?rywp80|Sy}|2ndc{C&r-i|bp_c@-fW4iqTTuC1ra{* zZQML`z5Qkzhha19xIxqH;V87jzUzC=_WDrru_Q%{Dr zj3I(7A-M*sfmw+1QfXOH%_TBRXMilFiO#8ou3$Fhk|E9KbvhwRAqM7@<^nHUd&@@) zmn(x|KTeQMQi2l?e1{O1rX}@* - diff --git a/src/SonarQube.Client.Tests/packages.lock.json b/src/SonarQube.Client.Tests/packages.lock.json index 011570e3b7..bd1e0dee0d 100644 --- a/src/SonarQube.Client.Tests/packages.lock.json +++ b/src/SonarQube.Client.Tests/packages.lock.json @@ -14,12 +14,6 @@ "resolved": "0.11.4", "contentHash": "zSCkwOgc5OyfMfEeMr9x0K7WCDf8i6VdF2RtCLN/4m6iebTtJQdeoJ9IS4/RyYHuLUYjrm0sd+siWbaSvSzRYQ==" }, - "Google.Protobuf": { - "type": "Direct", - "requested": "[3.6.1, )", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -83,11 +77,6 @@ "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "16.6.1", @@ -165,8 +154,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )" diff --git a/src/SonarQube.Client/Api/DefaultConfiguration.cs b/src/SonarQube.Client/Api/DefaultConfiguration.cs index ab56496060..2821988726 100644 --- a/src/SonarQube.Client/Api/DefaultConfiguration.cs +++ b/src/SonarQube.Client/Api/DefaultConfiguration.cs @@ -29,8 +29,7 @@ public static RequestFactory ConfigureSonarQube(RequestFactory requestFactory) requestFactory .RegisterRequest("2.1") .RegisterRequest("3.3") - .RegisterRequest("6.6") - .RegisterRequest("6.6"); + .RegisterRequest("6.6"); return requestFactory; } @@ -40,8 +39,7 @@ public static UnversionedRequestFactory ConfigureSonarCloud(UnversionedRequestFa requestFactory .RegisterRequest() .RegisterRequest() - .RegisterRequest() - .RegisterRequest(); + .RegisterRequest(); return requestFactory; } diff --git a/src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs b/src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs deleted file mode 100644 index ebbe0d8626..0000000000 --- a/src/SonarQube.Client/Api/V6_60/GetProjectBranchesRequest.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SonarQube.Client.Models; -using SonarQube.Client.Requests; - -namespace SonarQube.Client.Api.V6_60 -{ - public class GetProjectBranchesRequest : RequestBase, IGetProjectBranchesRequest - { - [JsonProperty("project")] - public virtual string ProjectKey { get; set; } - - protected override string Path => "api/project_branches/list"; - - protected override SonarQubeProjectBranch[] ParseResponse(string response) => - JObject.Parse(response)["branches"] - .ToObject() - .Select(ToProjectBranch) - .ToArray(); - - private SonarQubeProjectBranch ToProjectBranch(ServerProjectBranch serverBranch) => - new SonarQubeProjectBranch(serverBranch.Name, serverBranch.IsMain, serverBranch.AnalysisDate, serverBranch.Type); - - private sealed class ServerProjectBranch - { - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("isMain")] - public bool IsMain { get; set; } - - [JsonProperty("analysisDate")] - public DateTimeOffset AnalysisDate { get; set; } - } - } -} diff --git a/src/SonarQube.Client/ISonarQubeService.cs b/src/SonarQube.Client/ISonarQubeService.cs index 20e026b2f6..57b056f4c9 100644 --- a/src/SonarQube.Client/ISonarQubeService.cs +++ b/src/SonarQube.Client/ISonarQubeService.cs @@ -45,9 +45,4 @@ Task> GetNotificationEventsAsync( /// /// The method does not check whether the project or issue exists or not Uri GetViewIssueUrl(string projectKey, string issueKey); - - /// - /// Returns branch information for the specified project key - /// - Task> GetProjectBranchesAsync(string projectKey, CancellationToken token); } diff --git a/src/SonarQube.Client/Messages/Protobuf/.gitignore b/src/SonarQube.Client/Messages/Protobuf/.gitignore deleted file mode 100644 index 8143e15f93..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.cs diff --git a/src/SonarQube.Client/Messages/Protobuf/README.md b/src/SonarQube.Client/Messages/Protobuf/README.md deleted file mode 100644 index ed10249ee5..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/README.md +++ /dev/null @@ -1,2 +0,0 @@ -.proto file copied from -https://github.com/SonarSource/sonarqube/blob/master/sonar-scanner-protocol/src/main/protobuf/scanner_input.proto diff --git a/src/SonarQube.Client/Messages/Protobuf/scanner_input.proto b/src/SonarQube.Client/Messages/Protobuf/scanner_input.proto deleted file mode 100644 index 0a9b41d453..0000000000 --- a/src/SonarQube.Client/Messages/Protobuf/scanner_input.proto +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -/* -Notes - - - "required" fields are not used as recommended by Google to keep forward-compatibility: - https://developers.google.com/protocol-buffers/docs/proto#simple - - - this is beta version of specification. It will evolve during next releases and is - not forward-compatible yet. -*/ - -syntax = "proto3"; - -package SonarQube.Client.Messages; - -import "constants.proto"; - -option java_package = "org.sonar.scanner.protocol.input"; -option optimize_for = SPEED; - -message ServerIssue { - string key = 1; - string module_key = 2; - string path = 3; - string rule_repository = 4; - string rule_key = 5; - int32 line = 6; - string msg = 7; - Severity severity = 8; - bool manual_severity = 9; - string resolution = 10; - string status = 11; - string checksum = 12; - string assignee_login = 13; - int64 creation_date = 14; - string type = 15; -} \ No newline at end of file diff --git a/src/SonarQube.Client/SonarQube.Client.csproj b/src/SonarQube.Client/SonarQube.Client.csproj index e419df30f3..898a629587 100644 --- a/src/SonarQube.Client/SonarQube.Client.csproj +++ b/src/SonarQube.Client/SonarQube.Client.csproj @@ -10,36 +10,12 @@ - - - - - - - - - true - - - - true - - + - - - - - "$(UserProfile)/.nuget/packages/Grpc.Tools/1.4.1/tools/windows_x64/protoc.exe" - - - - - \ No newline at end of file diff --git a/src/SonarQube.Client/SonarQubeService.cs b/src/SonarQube.Client/SonarQubeService.cs index 4c462b661b..f72ec38cd2 100644 --- a/src/SonarQube.Client/SonarQubeService.cs +++ b/src/SonarQube.Client/SonarQubeService.cs @@ -132,13 +132,6 @@ public Uri GetViewIssueUrl(string projectKey, string issueKey) return new Uri(currentHttpClient.BaseAddress, string.Format(ViewIssueRelativeUrl, projectKey, issueKey)); } - public async Task> GetProjectBranchesAsync(string projectKey, CancellationToken token) => - await InvokeCheckedRequestAsync( - request => - { - request.ProjectKey = projectKey; - }, token); - /// /// Creates a new instance of the specified TRequest request, configures and invokes it and returns its response. /// diff --git a/src/SonarQube.Client/packages.config b/src/SonarQube.Client/packages.config deleted file mode 100644 index c0e2b232d4..0000000000 --- a/src/SonarQube.Client/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/SonarQube.Client/packages.lock.json b/src/SonarQube.Client/packages.lock.json index d968ad250a..c5ab67976c 100644 --- a/src/SonarQube.Client/packages.lock.json +++ b/src/SonarQube.Client/packages.lock.json @@ -2,18 +2,6 @@ "version": 1, "dependencies": { ".NETFramework,Version=v4.7.2": { - "Google.Protobuf": { - "type": "Direct", - "requested": "[3.6.1, )", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Direct", - "requested": "[1.4.1, )", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "Newtonsoft.Json": { "type": "Direct", "requested": "[13.0.3, )", diff --git a/src/TestInfrastructure/packages.lock.json b/src/TestInfrastructure/packages.lock.json index 40fcb507bd..d6657792a9 100644 --- a/src/TestInfrastructure/packages.lock.json +++ b/src/TestInfrastructure/packages.lock.json @@ -221,16 +221,6 @@ "Microsoft.VisualStudio.Interop": "17.0.31902.203" } }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==" - }, - "Grpc.Tools": { - "type": "Transitive", - "resolved": "1.4.1", - "contentHash": "D5AcNr0yPFz5dqftJYKnMtwg6AEMUics+UysxTXKVuZtresqWUcHIrnscM+KsAIreG7wvdumWzjdIXRIMekCLg==" - }, "MessagePack": { "type": "Transitive", "resolved": "2.2.85", @@ -1291,8 +1281,6 @@ "sonarqube.client": { "type": "Project", "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Grpc.Tools": "[1.4.1, )", "Newtonsoft.Json": "[13.0.3, )", "SonarLint.VisualStudio.Core": "[1.0.0, )", "System.Net.Http": "[4.0.0, )"