Skip to content

Commit 6a0ce78

Browse files
SLVS-2465 Add analysis engine to connect with Roslyn APIs (#6368)
[SLVS-2465](https://sonarsource.atlassian.net/browse/SLVS-2465) Part of <!-- Only for standalone PRs without Jira issue in the PR title: * Replace this comment with Epic ID to create a new Task in Jira * Replace this comment with Issue ID to create a new Sub-Task in Jira * Ignore or delete this note to create a new Task in Jira without a parent --> [SLVS-2465]: https://sonarsource.atlassian.net/browse/SLVS-2465?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 642a707 commit 6a0ce78

File tree

41 files changed

+2509
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2509
-10
lines changed

src/Core.UnitTests/SonarCompositeRuleIdTests.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21-
using System;
22-
using FluentAssertions;
23-
using Microsoft.VisualStudio.TestTools.UnitTesting;
24-
2521
namespace SonarLint.VisualStudio.Core.UnitTests
2622
{
2723
[TestClass]
@@ -85,7 +81,20 @@ public void ToString_ReturnsExpected()
8581
output.ToString().Should().Be(input);
8682
}
8783

84+
[TestMethod]
85+
[DataRow("repo1", "rule1", "repo1:rule1")]
86+
[DataRow("csharpsquid", "S1234", "csharpsquid:S1234")]
87+
[DataRow("javascript", "S5678", "javascript:S5678")]
88+
[DataRow("my repo", "my rule", "my repo:my rule")]
89+
public void GetFullErrorCode_ConcatenatesWithColon(string repoKey, string ruleKey, string expected)
90+
{
91+
var result = SonarCompositeRuleId.GetFullErrorCode(repoKey, ruleKey);
92+
93+
result.Should().Be(expected);
94+
}
95+
8896
public static object[][] ReposAndLanguages => LanguageProvider.Instance.AllKnownLanguages.Select(x => new object[] { x.RepoInfo.Key, x }).ToArray();
97+
8998
[DataTestMethod]
9099
[DynamicData(nameof(ReposAndLanguages))]
91100
public void Language_MatchesBasedOnRepoKey(string repoKey, Language language)

src/Core/SonarCompositeRuleId.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ public SonarCompositeRuleId(string repoKey, string ruleKey)
4747
{
4848
RepoKey = repoKey ?? throw new ArgumentNullException(nameof(repoKey));
4949
RuleKey = ruleKey ?? throw new ArgumentNullException(nameof(ruleKey));
50-
ErrorListErrorCode = repoKey + Separator + ruleKey;
50+
ErrorListErrorCode = GetFullErrorCode(repoKey, ruleKey);
5151
Language = LanguageProvider.Instance.AllKnownLanguages.FirstOrDefault(x => x.RepoInfo.Key == repoKey) ?? Language.Unknown;
5252
}
5353

54+
public static string GetFullErrorCode(string repoKey, string ruleKey) => repoKey + Separator + ruleKey;
55+
5456
public string ErrorListErrorCode { get; }
5557
public string RepoKey { get; }
5658
public string RuleKey { get; }

src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
################################
33
# Assembly references report
4-
# Report date/time: 2025-08-13T14:59:53.8952119Z
4+
# Report date/time: 2025-08-15T10:08:11.0508610Z
55
################################
66
#
77
# Generated by Devtility CheckAsmRefs v0.11.0.223
@@ -309,13 +309,18 @@ Assembly: 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.24.0.0, Cultur
309309
Relative path: 'SonarLint.VisualStudio.RoslynAnalyzerServer.dll'
310310

311311
Referenced assemblies:
312+
- 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
313+
- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
314+
- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
312315
- 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
313316
- 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'
314317
- 'SonarLint.VisualStudio.Core, Version=8.24.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244'
315318
- 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
319+
- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
316320
- 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
321+
- 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
317322
- 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
318-
# Number of references: 6
323+
# Number of references: 11
319324

320325
---
321326
Assembly: 'SonarLint.VisualStudio.SLCore, Version=8.24.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244'

src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
################################
33
# Assembly references report
4-
# Report date/time: 2025-08-13T14:59:53.8952119Z
4+
# Report date/time: 2025-08-15T10:08:11.0508610Z
55
################################
66
#
77
# Generated by Devtility CheckAsmRefs v0.11.0.223
@@ -309,13 +309,18 @@ Assembly: 'SonarLint.VisualStudio.RoslynAnalyzerServer, Version=8.24.0.0, Cultur
309309
Relative path: 'SonarLint.VisualStudio.RoslynAnalyzerServer.dll'
310310

311311
Referenced assemblies:
312+
- 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
313+
- 'Microsoft.CodeAnalysis.Workspaces, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
314+
- 'Microsoft.VisualStudio.LanguageServices, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
312315
- 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
313316
- 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'
314317
- 'SonarLint.VisualStudio.Core, Version=8.24.0.0, Culture=neutral, PublicKeyToken=null'
315318
- 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
319+
- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
316320
- 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
321+
- 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
317322
- 'System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
318-
# Number of references: 6
323+
# Number of references: 11
319324

320325
---
321326
Assembly: 'SonarLint.VisualStudio.SLCore, Version=8.24.0.0, Culture=neutral, PublicKeyToken=null'
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* SonarLint for Visual Studio
3+
* Copyright (C) 2016-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis;
22+
23+
namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis;
24+
25+
[TestClass]
26+
public class DiagnosticDuplicatesComparerTests
27+
{
28+
private readonly DiagnosticDuplicatesComparer testSubject = DiagnosticDuplicatesComparer.Instance;
29+
private readonly RoslynIssue diagnostic1 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10);
30+
31+
[TestMethod]
32+
public void Equals_SameReference_ReturnsTrue()
33+
{
34+
var result = testSubject.Equals(diagnostic1, diagnostic1);
35+
36+
result.Should().BeTrue();
37+
}
38+
39+
[TestMethod]
40+
public void Equals_FirstArgumentNull_ReturnsFalse()
41+
{
42+
var result = testSubject.Equals(null, diagnostic1);
43+
44+
result.Should().BeFalse();
45+
}
46+
47+
[TestMethod]
48+
public void Equals_SecondArgumentNull_ReturnsFalse()
49+
{
50+
var result = testSubject.Equals(diagnostic1, null);
51+
52+
result.Should().BeFalse();
53+
}
54+
55+
[TestMethod]
56+
public void Equals_SameRuleKeyAndLocation_ReturnsTrue()
57+
{
58+
var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10);
59+
60+
var result = testSubject.Equals(diagnostic1, diagnostic2);
61+
62+
result.Should().BeTrue();
63+
}
64+
65+
66+
[TestMethod]
67+
public void Equals_SameRuleKeyAndLocation_MessageIsIgnored()
68+
{
69+
var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10, "some different message");
70+
71+
var result = testSubject.Equals(diagnostic1, diagnostic2);
72+
73+
result.Should().BeTrue();
74+
}
75+
76+
[TestMethod]
77+
[DataRow("rule2", "file1.cs", 1, 1, 1, 10, DisplayName = "Different RuleKey")]
78+
[DataRow("rule1", "file2.cs", 1, 1, 1, 10, DisplayName = "Different FilePath")]
79+
[DataRow("rule1", "file1.cs", 2, 1, 1, 10, DisplayName = "Different StartLine")]
80+
[DataRow("rule1", "file1.cs", 1, 1, 2, 10, DisplayName = "Different EndLine")]
81+
[DataRow("rule1", "file1.cs", 1, 2, 1, 10, DisplayName = "Different StartLineOffset")]
82+
[DataRow("rule1", "file1.cs", 1, 1, 1, 11, DisplayName = "Different EndLineOffset")]
83+
public void Equals_DifferentValues_ReturnsFalse(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset)
84+
{
85+
var diagnostic2 = CreateDiagnostic(ruleKey, filePath, startLine, startLineOffset, endLine, endLineOffset);
86+
87+
var result = testSubject.Equals(diagnostic1, diagnostic2);
88+
89+
result.Should().BeFalse();
90+
}
91+
92+
[TestMethod]
93+
public void GetHashCode_SameObjects_ReturnsSameHashCode()
94+
{
95+
var diagnostic2 = CreateDiagnostic("rule1", "file1.cs", 1, 1, 1, 10);
96+
97+
var hash1 = testSubject.GetHashCode(diagnostic1);
98+
var hash2 = testSubject.GetHashCode(diagnostic2);
99+
100+
hash1.Should().Be(hash2);
101+
}
102+
103+
[TestMethod]
104+
public void GetHashCode_DifferentObjects_ReturnsDifferentHashCodes()
105+
{
106+
var diagnostic2 = CreateDiagnostic("rule2", "file2.cs", 2, 2, 2, 20);
107+
108+
var hash1 = testSubject.GetHashCode(diagnostic1);
109+
var hash2 = testSubject.GetHashCode(diagnostic2);
110+
111+
hash1.Should().NotBe(hash2);
112+
}
113+
114+
[TestMethod]
115+
public void Instance_ReturnsSingletonInstance()
116+
{
117+
var instance1 = DiagnosticDuplicatesComparer.Instance;
118+
var instance2 = DiagnosticDuplicatesComparer.Instance;
119+
120+
instance1.Should().BeSameAs(instance2);
121+
}
122+
123+
private static RoslynIssue CreateDiagnostic(string ruleKey, string filePath, int startLine, int startLineOffset, int endLine, int endLineOffset, string? message = null)
124+
{
125+
var textRange = new RoslynIssueTextRange(startLine, endLine, startLineOffset, endLineOffset);
126+
var location = new RoslynIssueLocation(message ?? "message", filePath, textRange);
127+
return new RoslynIssue(ruleKey, location);
128+
}
129+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* SonarLint for Visual Studio
3+
* Copyright (C) 2016-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
using Microsoft.CodeAnalysis;
22+
using Microsoft.CodeAnalysis.Text;
23+
using SonarLint.VisualStudio.Core;
24+
using SonarLint.VisualStudio.RoslynAnalyzerServer.Analysis;
25+
using SonarLint.VisualStudio.TestInfrastructure;
26+
27+
namespace SonarLint.VisualStudio.RoslynAnalyzerServer.UnitTests.Analysis;
28+
29+
[TestClass]
30+
public class DiagnosticToRoslynIssueConverterTests
31+
{
32+
private readonly DiagnosticToRoslynIssueConverter testSubject = new();
33+
34+
[TestMethod]
35+
public void MefCtor_CheckExports() =>
36+
MefTestHelpers.CheckTypeCanBeImported<DiagnosticToRoslynIssueConverter, IDiagnosticToRoslynIssueConverter>();
37+
38+
[TestMethod]
39+
public void MefCtor_CheckIsSingleton() => MefTestHelpers.CheckIsSingletonMefComponent<DiagnosticToRoslynIssueConverter>();
40+
41+
public static object[][] TestData =>
42+
[
43+
[Language.CSharp, "S1234", "test message", "c:\\test\\file.cs", 0, 0, 3, 4],
44+
[Language.CSharp, "S1234", "test message", "c:\\test\\file.cs", 1, 1, 3, 4],
45+
[Language.CSharp, "S5678", "multi-line issue", "c:\\test\\file2.cs", 5, 10, 15, 20],
46+
[Language.VBNET, "S1234", "test message", "c:\\test\\file.vb", 0, 0, 3, 4]
47+
];
48+
49+
[DataTestMethod]
50+
[DynamicData(nameof(TestData))]
51+
public void ConvertToSonarDiagnostic_ConvertsDiagnosticCorrectly(
52+
Language language, string ruleId, string message, string filePath,
53+
int startLine, int endLine, int startChar, int endChar)
54+
{
55+
var diagnostic = CreateDiagnostic(ruleId, message, filePath, startLine, endLine, startChar, endChar);
56+
var expectedTextRange = new RoslynIssueTextRange(
57+
startLine + 1, // Convert to 1-based
58+
endLine + 1, // Convert to 1-based
59+
startChar,
60+
endChar);
61+
var expectedLocation = new RoslynIssueLocation(
62+
message,
63+
filePath,
64+
expectedTextRange);
65+
var expectedRuleId = $"{language.RepoInfo.Key}:{ruleId}";
66+
var expectedDiagnostic = new RoslynIssue(
67+
expectedRuleId,
68+
expectedLocation);
69+
70+
var result = testSubject.ConvertToSonarDiagnostic(diagnostic, language);
71+
72+
result.Should().BeEquivalentTo(expectedDiagnostic);
73+
}
74+
75+
private static Diagnostic CreateDiagnostic(string id, string message, string filePath, int startLine, int endLine, int startChar, int endChar)
76+
{
77+
var descriptor = new DiagnosticDescriptor(
78+
id,
79+
"Any Title",
80+
message,
81+
"Any Category",
82+
default,
83+
default);
84+
85+
var location = CreateLocation(filePath, startLine, endLine, startChar, endChar);
86+
87+
return Diagnostic.Create(descriptor, location);
88+
}
89+
90+
private static Location CreateLocation(string filePath, int startLine, int endLine, int startChar, int endChar)
91+
{
92+
var textSpan = new TextSpan(12, 34);
93+
var syntaxTree = Substitute.For<SyntaxTree>();
94+
var linePositionSpan = new LinePositionSpan(
95+
new LinePosition(startLine, startChar),
96+
new LinePosition(endLine, endChar));
97+
syntaxTree.GetMappedLineSpan(textSpan, CancellationToken.None).Returns(new FileLinePositionSpan(filePath, linePositionSpan));
98+
99+
return Location.Create(syntaxTree, textSpan);
100+
}
101+
}

0 commit comments

Comments
 (0)