-
Notifications
You must be signed in to change notification settings - Fork 708
Expand file tree
/
Copy pathMCPEXP001SuppressorTests.cs
More file actions
159 lines (138 loc) · 5.88 KB
/
MCPEXP001SuppressorTests.cs
File metadata and controls
159 lines (138 loc) · 5.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using Xunit;
namespace ModelContextProtocol.Analyzers.Tests;
public class MCPEXP001SuppressorTests
{
[Fact]
public async Task Suppressor_InGeneratedCode_SuppressesMCPEXP001()
{
// Simulate source-generated code (e.g., STJ source gen) that references an experimental type.
// The file path ends with .g.cs to indicate it's generated.
var result = await RunSuppressorAsync(
source: """
using ExperimentalTypes;
namespace Generated
{
public static class SerializerHelper
{
public static object Create() => new ExperimentalClass();
}
}
""",
filePath: "Generated.g.cs",
additionalSource: GetExperimentalTypeDefinition(),
additionalFilePath: "ExperimentalTypes.cs");
// MCPEXP001 should exist before the suppressor runs
Assert.Contains(result.BeforeSuppression, d => d.Id == "MCPEXP001");
// After suppression, MCPEXP001 should be gone from the results
Assert.DoesNotContain(result.AfterSuppression, d => d.Id == "MCPEXP001");
}
[Fact]
public async Task Suppressor_InHandWrittenCode_DoesNotSuppressMCPEXP001()
{
// Hand-written user code referencing an experimental type.
// The file path does NOT end with .g.cs.
var result = await RunSuppressorAsync(
source: """
using ExperimentalTypes;
namespace UserCode
{
public static class MyHelper
{
public static object Create() => new ExperimentalClass();
}
}
""",
filePath: "MyHelper.cs",
additionalSource: GetExperimentalTypeDefinition(),
additionalFilePath: "ExperimentalTypes.cs");
// MCPEXP001 should exist before the suppressor runs
Assert.Contains(result.BeforeSuppression, d => d.Id == "MCPEXP001");
// It should still be present after the suppressor runs (not suppressed)
Assert.Contains(result.AfterSuppression, d => d.Id == "MCPEXP001");
}
[Fact]
public async Task Suppressor_MixedGeneratedAndHandWritten_OnlySuppressesGenerated()
{
var result = await RunSuppressorAsync(
[
(GetExperimentalTypeDefinition(), "ExperimentalTypes.cs"),
("""
using ExperimentalTypes;
namespace Generated
{
public static class GeneratedHelper
{
public static object Create() => new ExperimentalClass();
}
}
""", "Generated.g.cs"),
("""
using ExperimentalTypes;
namespace UserCode
{
public static class UserHelper
{
public static object Create() => new ExperimentalClass();
}
}
""", "UserCode.cs"),
]);
// Should have MCPEXP001 in both files before suppression
Assert.Equal(2, result.BeforeSuppression.Count(d => d.Id == "MCPEXP001"));
// After suppression: only the hand-written one should remain
var remaining = result.AfterSuppression.Where(d => d.Id == "MCPEXP001").ToList();
Assert.Single(remaining);
Assert.Equal("UserCode.cs", remaining[0].Location.SourceTree?.FilePath);
}
private static string GetExperimentalTypeDefinition() => """
using System.Diagnostics.CodeAnalysis;
namespace ExperimentalTypes
{
[Experimental("MCPEXP001")]
public class ExperimentalClass { }
}
""";
private static Task<SuppressorResult> RunSuppressorAsync(
string source,
string filePath,
string additionalSource,
string additionalFilePath)
{
return RunSuppressorAsync([(additionalSource, additionalFilePath), (source, filePath)]);
}
private static async Task<SuppressorResult> RunSuppressorAsync(params (string Source, string FilePath)[] sources)
{
var syntaxTrees = sources.Select(
s => CSharpSyntaxTree.ParseText(s.Source, path: s.FilePath)).ToArray();
var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
List<MetadataReference> referenceList =
[
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll")),
MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll")),
];
var compilation = CSharpCompilation.Create(
"TestAssembly",
syntaxTrees,
referenceList,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var beforeSuppression = compilation.GetDiagnostics();
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(new MCPEXP001Suppressor());
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
var afterSuppression = await compilationWithAnalyzers.GetAllDiagnosticsAsync(default);
return new SuppressorResult
{
BeforeSuppression = beforeSuppression,
AfterSuppression = afterSuppression,
};
}
private class SuppressorResult
{
public ImmutableArray<Diagnostic> BeforeSuppression { get; set; } = [];
public ImmutableArray<Diagnostic> AfterSuppression { get; set; } = [];
}
}