Skip to content

Commit 68b06ac

Browse files
committed
Bug fixes and static analyzer for more safety.
1 parent f8ac9b8 commit 68b06ac

29 files changed

+1697
-639
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
<LangVersion>12</LangVersion>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
16+
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.10.0" />
17+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
18+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.2" />
19+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
20+
<PackageReference Include="NUnit" Version="3.14.0" />
21+
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
22+
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\Hexa.NET.Unsafe.Analyzers\Hexa.NET.Unsafe.Analyzers.csproj" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<Using Include="NUnit.Framework" />
31+
</ItemGroup>
32+
33+
</Project>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
namespace Hexa.NET.Unsafe.Analyzers.Tests
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Testing;
5+
using NUnit.Framework;
6+
using System.Threading.Tasks;
7+
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<MemberNotFreedAnalyzer>;
8+
9+
[TestFixture]
10+
public class MemberNotFreedAnalyzerTests
11+
{
12+
private static readonly string mock = @"
13+
namespace Hexa.NET.Utilities
14+
{
15+
using System;
16+
17+
public static unsafe class Utils
18+
{
19+
public static unsafe void* Alloc() => null;
20+
public static unsafe T* AllocT<T>() where T : unmanaged
21+
{
22+
return null;
23+
}
24+
public static unsafe void Free(void* ptr) { }
25+
}
26+
27+
[System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
28+
public sealed class SuppressFreeWarningAttribute : System.Attribute { }
29+
}
30+
";
31+
32+
private static DiagnosticResult ExpectedWarning(string memberName) =>
33+
new DiagnosticResult("MNF001", DiagnosticSeverity.Warning)
34+
.WithMessage($"The member '{memberName}' is not freed in any method of the containing class/struct");
35+
36+
[Test]
37+
public async Task TestMemberNotFreed_ShouldWarn()
38+
{
39+
var testCode = @"
40+
unsafe class TestClass
41+
{
42+
private int* _ptr;
43+
44+
public unsafe void Allocate()
45+
{
46+
_ptr = AllocT<int>();
47+
}
48+
}";
49+
50+
await VerifyAnalyzerAsync(testCode, ExpectedWarning("_ptr").WithSpan(10, 5, 10, 23));
51+
}
52+
53+
[Test]
54+
public async Task TestMemberFreed_ShouldNotWarn()
55+
{
56+
var testCode = @"
57+
unsafe class TestClass
58+
{
59+
private int* _ptr;
60+
61+
public unsafe void Allocate()
62+
{
63+
_ptr = AllocT<int>();
64+
}
65+
66+
public unsafe void Deallocate()
67+
{
68+
Free(_ptr);
69+
}
70+
}";
71+
72+
await VerifyAnalyzerAsync(testCode);
73+
}
74+
75+
[Test]
76+
public async Task TestSuppressionAttributeOnField_ShouldNotWarn()
77+
{
78+
var testCode = @"
79+
unsafe class TestClass
80+
{
81+
[SuppressFreeWarning]
82+
private int* _ptr;
83+
84+
public unsafe void Allocate()
85+
{
86+
_ptr = AllocT<int>();
87+
}
88+
}";
89+
90+
await VerifyAnalyzerAsync(testCode);
91+
}
92+
93+
[Test]
94+
public async Task TestMultipleFieldsOneNotFreed_ShouldWarn()
95+
{
96+
var testCode = @"
97+
unsafe class TestClass
98+
{
99+
private int* _ptr1;
100+
private float* _ptr2;
101+
102+
public unsafe void Allocate()
103+
{
104+
_ptr1 = AllocT<int>();
105+
_ptr2 = AllocT<float>();
106+
}
107+
108+
public unsafe void Deallocate()
109+
{
110+
Free(_ptr1);
111+
}
112+
}";
113+
114+
await VerifyAnalyzerAsync(testCode, ExpectedWarning("_ptr2").WithSpan(11, 5, 11, 26));
115+
}
116+
117+
private static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
118+
{
119+
source = $@"
120+
namespace TestNamespace
121+
{{
122+
using static Hexa.NET.Utilities.Utils;
123+
using Hexa.NET.Utilities;
124+
125+
{source}
126+
}}
127+
";
128+
129+
source += mock; // appending makes analysis more stable
130+
await VerifyCS.VerifyAnalyzerAsync(source, expected);
131+
}
132+
}
133+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
namespace Hexa.NET.Unsafe.Analyzers.Tests
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Testing;
5+
using NUnit.Framework;
6+
using System.Threading.Tasks;
7+
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<PointerFreeAnalyzer>;
8+
9+
[TestFixture]
10+
public class PointerFreeAnalyzerTests
11+
{
12+
private static DiagnosticResult ExpectedWarning(string methodName) =>
13+
new DiagnosticResult("PFA001", DiagnosticSeverity.Warning)
14+
.WithMessage($"Memory allocated with {methodName} is not freed or written to a field");
15+
16+
private static readonly string mock = @"
17+
namespace Hexa.NET.Utilities
18+
{
19+
public static unsafe class Utils
20+
{
21+
public static unsafe void* Alloc() => null;
22+
public static unsafe T* AllocT<T>() where T : unmanaged
23+
{
24+
return null;
25+
}
26+
public static unsafe void Free(void* ptr) { }
27+
}
28+
}
29+
";
30+
31+
[Test]
32+
public async Task TestAllocWithoutFreeOrFieldAssignment_ShouldWarn()
33+
{
34+
string path = "../../../../Hexa.NET.Utilities/Utils.cs";
35+
36+
path = Path.GetFullPath(path);
37+
38+
var testCode = @"
39+
unsafe class TestClass
40+
{
41+
unsafe void TestMethod()
42+
{
43+
var ptr = Alloc();
44+
}
45+
}";
46+
47+
await VerifyAnalyzerAsync(testCode, ExpectedWarning("Alloc").WithSpan(12, 19, 12, 26));
48+
}
49+
50+
[Test]
51+
public async Task TestAllocWithFree_ShouldNotWarn()
52+
{
53+
var testCode = @"
54+
unsafe class TestClass
55+
{
56+
unsafe void TestMethod()
57+
{
58+
var ptr = Alloc();
59+
Free(ptr);
60+
}
61+
}";
62+
63+
await VerifyAnalyzerAsync(testCode);
64+
}
65+
66+
[Test]
67+
public async Task TestAllocWithFieldAssignment_ShouldNotWarn()
68+
{
69+
var testCode = @"
70+
unsafe class TestClass
71+
{
72+
private void* _ptr;
73+
74+
unsafe void TestMethod()
75+
{
76+
_ptr = Alloc();
77+
}
78+
}";
79+
80+
await VerifyAnalyzerAsync(testCode);
81+
}
82+
83+
[Test]
84+
public async Task TestAllocWithPropAssignment_ShouldNotWarn()
85+
{
86+
var testCode = @"
87+
unsafe class TestClass
88+
{
89+
private void* _ptr { get; set; }
90+
91+
unsafe void TestMethod()
92+
{
93+
_ptr = Alloc();
94+
}
95+
}";
96+
97+
await VerifyAnalyzerAsync(testCode);
98+
}
99+
100+
private static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
101+
{
102+
source = $@"
103+
namespace TestNamespace
104+
{{
105+
using static Hexa.NET.Utilities.Utils;
106+
using Hexa.NET.Utilities;
107+
108+
{source}
109+
}}
110+
";
111+
112+
source = source + mock;
113+
await VerifyCS.VerifyAnalyzerAsync(source, expected);
114+
}
115+
}
116+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
namespace Hexa.NET.Unsafe.Analyzers.Tests
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Testing;
5+
using NUnit.Framework;
6+
using System.Threading.Tasks;
7+
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<ReadonlyFreeableStructAnalyzer>;
8+
9+
[TestFixture]
10+
public class ReadonlyFreeableStructAnalyzerTests
11+
{
12+
private static readonly string mock = @"
13+
namespace Hexa.NET.Utilities
14+
{
15+
public static class Utils { }
16+
17+
public interface IFreeable
18+
{
19+
void Release();
20+
}
21+
22+
public struct FreeableStruct : IFreeable
23+
{
24+
public void Release() { }
25+
}
26+
27+
public struct NonFreeableStruct
28+
{
29+
public int Value;
30+
}
31+
}
32+
";
33+
34+
private static DiagnosticResult ExpectedWarning(string structName) =>
35+
new DiagnosticResult("RFS001", DiagnosticSeverity.Warning)
36+
.WithMessage($"The struct '{structName}' implements IFreeable and should not be used with the readonly modifier");
37+
38+
[Test]
39+
public async Task TestReadonlyFreeableStruct_ShouldWarn()
40+
{
41+
var testCode = @"
42+
class TestClass
43+
{
44+
private readonly FreeableStruct _freeable;
45+
}";
46+
47+
await VerifyAnalyzerAsync(testCode, ExpectedWarning("FreeableStruct").WithSpan(10, 37, 10, 46));
48+
}
49+
50+
[Test]
51+
public async Task TestNonReadonlyFreeableStruct_ShouldNotWarn()
52+
{
53+
var testCode = @"
54+
class TestClass
55+
{
56+
private FreeableStruct _freeable;
57+
}";
58+
59+
await VerifyAnalyzerAsync(testCode);
60+
}
61+
62+
[Test]
63+
public async Task TestReadonlyNonFreeableStruct_ShouldNotWarn()
64+
{
65+
var testCode = @"
66+
class TestClass
67+
{
68+
private readonly NonFreeableStruct _nonFreeable;
69+
}";
70+
71+
await VerifyAnalyzerAsync(testCode);
72+
}
73+
74+
private static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
75+
{
76+
source = $@"
77+
namespace TestNamespace
78+
{{
79+
using static Hexa.NET.Utilities.Utils;
80+
using Hexa.NET.Utilities;
81+
82+
{source}
83+
}}
84+
";
85+
86+
source += mock; // appending makes analysis more stable
87+
await VerifyCS.VerifyAnalyzerAsync(source, expected);
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)