Skip to content

Commit 317c7a2

Browse files
committed
Update analyzer to support [TestingAndPrivateOnly] attribute
1 parent c7dd7cd commit 317c7a2

File tree

2 files changed

+114
-16
lines changed

2 files changed

+114
-16
lines changed

tracer/src/Datadog.Trace.Tools.Analyzers/TestingOnlyAnalyzer/TestingOnlyAnalyzer.cs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ namespace Datadog.Trace.Tools.Analyzers.TestingOnlyAnalyzer
1919
/// <summary>
2020
/// DD002: Incorrect usage of internal API
2121
///
22-
/// Finds internal usages of APIs specifically marked with the [TestingOnly] flag.
23-
/// These methods should not be called directly by our library code, only from test code.
24-
/// The analyzer enforces that requirement
22+
/// Finds internal usages of APIs specifically marked with the [TestingOnly] or [TestingAndPrivateOnly] flag.
23+
/// - [TestingOnly]: Methods should not be called directly by our library code, only from test code.
24+
/// - [TestingAndPrivateOnly]: Methods can be called from within the same type, or from test code, but not from other types.
25+
/// The analyzer enforces these requirements.
2526
///
2627
/// </summary>
2728
[DiagnosticAnalyzer(LanguageNames.CSharp)]
@@ -33,9 +34,7 @@ public sealed class TestingOnlyAnalyzer : DiagnosticAnalyzer
3334
public const string DiagnosticId = "DD0002";
3435

3536
private const string TestingOnlyAttribute = nameof(TestingOnlyAttribute);
36-
37-
private static readonly ImmutableArray<string> TestingOnlyAttributeNames
38-
= ImmutableArray.Create(TestingOnlyAttribute);
37+
private const string TestingAndPrivateOnlyAttribute = nameof(TestingAndPrivateOnlyAttribute);
3938

4039
#pragma warning disable RS2008 // Enable analyzer release tracking for the analyzer project
4140
private static readonly DiagnosticDescriptor Rule = new(
@@ -71,11 +70,12 @@ private static void AnalyzeOperationBlock(
7170
ConcurrentDictionary<ISymbol, InternalApiStatus> internalApiMembers)
7271
{
7372
var internalApiOperations = PooledConcurrentDictionary<KeyValuePair<IOperation, ISymbol>, bool>.GetInstance();
73+
var callingType = context.OwningSymbol?.ContainingType;
7474

7575
context.RegisterOperationAction(
7676
ctx =>
7777
{
78-
Helpers.AnalyzeOperation(ctx.Operation, internalApiOperations, internalApiMembers);
78+
Helpers.AnalyzeOperation(ctx.Operation, internalApiOperations, internalApiMembers, callingType);
7979
},
8080
OperationKind.MethodReference,
8181
OperationKind.EventReference,
@@ -110,7 +110,8 @@ private static class Helpers
110110
internal static void AnalyzeOperation(
111111
IOperation operation,
112112
PooledConcurrentDictionary<KeyValuePair<IOperation, ISymbol>, bool> internalApiOperations,
113-
ConcurrentDictionary<ISymbol, InternalApiStatus> internalApiMembers)
113+
ConcurrentDictionary<ISymbol, InternalApiStatus> internalApiMembers,
114+
INamedTypeSymbol? callingType)
114115
{
115116
var symbol = GetOperationSymbol(operation);
116117

@@ -185,8 +186,21 @@ void CheckTypeArgumentsCore(ImmutableArray<ITypeSymbol> typeArguments, PooledHas
185186

186187
void CheckOperationAttributes(ISymbol symbol, bool checkParents)
187188
{
188-
if (TryGetOrCreatePublicApiAttributes(symbol, checkParents, internalApiMembers, out _))
189+
if (TryGetOrCreatePublicApiAttributes(symbol, checkParents, internalApiMembers, out var attributes))
189190
{
191+
// If the marked member has [TestingAndPrivateOnly], allow calls from within the same type
192+
if (attributes.IsTestingAndPrivateOnly)
193+
{
194+
var targetType = symbol.ContainingType;
195+
196+
// Allow calls from within the same type
197+
if (callingType != null && targetType != null &&
198+
SymbolEqualityComparer.Default.Equals(callingType, targetType))
199+
{
200+
return;
201+
}
202+
}
203+
190204
internalApiOperations.TryAdd(new KeyValuePair<IOperation, ISymbol>(operation, symbol), true);
191205
}
192206
}
@@ -243,7 +257,8 @@ private static InternalApiStatus CopyAttributes(InternalApiStatus copyAttributes
243257
new()
244258
{
245259
IsAssemblyAttribute = copyAttributes.IsAssemblyAttribute,
246-
IsPublicApi = copyAttributes.IsPublicApi
260+
IsTestingApi = copyAttributes.IsTestingApi,
261+
IsTestingAndPrivateOnly = copyAttributes.IsTestingAndPrivateOnly
247262
};
248263

249264
private static bool IsWithinConditionalOperation(IFieldReferenceOperation pOperation) =>
@@ -287,17 +302,25 @@ private static bool TryGetOrCreatePublicApiAttributes(
287302
attributes = publicApiMembers.GetOrAdd(symbol, attributes);
288303
}
289304

290-
return attributes.IsPublicApi;
305+
return attributes.IsTestingApi;
291306

292307
static void MergePlatformAttributes(
293308
ImmutableArray<AttributeData> immediateAttributes,
294309
ref InternalApiStatus parentAttributes)
295310
{
296311
foreach (AttributeData attribute in immediateAttributes)
297312
{
298-
if (TestingOnlyAttributeNames.Contains(attribute.AttributeClass!.Name))
313+
var attributeName = attribute.AttributeClass!.Name;
314+
if (attributeName == TestingAndPrivateOnlyAttribute)
299315
{
300-
parentAttributes.IsPublicApi = true;
316+
parentAttributes.IsTestingApi = true;
317+
parentAttributes.IsTestingAndPrivateOnly = true;
318+
return;
319+
}
320+
else if (attributeName == TestingOnlyAttribute)
321+
{
322+
parentAttributes.IsTestingApi = true;
323+
parentAttributes.IsTestingAndPrivateOnly = false;
301324
return;
302325
}
303326
}
@@ -309,7 +332,9 @@ private sealed class InternalApiStatus
309332
{
310333
public bool IsAssemblyAttribute { get; set; }
311334

312-
public bool IsPublicApi { get; set; }
335+
public bool IsTestingApi { get; set; }
336+
337+
public bool IsTestingAndPrivateOnly { get; set; }
313338
}
314339
}
315340
}

tracer/test/Datadog.Trace.Tools.Analyzers.Tests/TestingOnlyAnalyzerTests.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class TestingOnlyAnalyzerTests
2323

2424
public static string[] GetTestingOnlyAttributes { get; } = { "TestingOnly", "TestingOnlyAttribute" };
2525

26+
public static string[] GetTestingAndPrivateOnlyAttributes { get; } = { "TestingAndPrivateOnly", "TestingAndPrivateOnlyAttribute" };
27+
2628
public static string[] NonTestingOnlyAccesses { get; } =
2729
{
2830
"var x = _nonPublicField;",
@@ -65,6 +67,26 @@ public class TestingOnlyAnalyzerTests
6567
""",
6668
};
6769

70+
public static string[] TestingAndPrivateOnlySameTypeAccesses { get; } =
71+
{
72+
// Access members from the same class - should NOT be flagged
73+
"var x = _publicField;",
74+
"var x = PublicProperty;",
75+
"PublicProperty = string.Empty;",
76+
"var x = PublicMethod();",
77+
"var x = new TestClass();",
78+
};
79+
80+
public static string[] TestingAndPrivateOnlyDifferentTypeAccesses { get; } =
81+
{
82+
// Access members from a different class - should be flagged
83+
"var x = {|#0:OtherClass._testingAndPrivateField|};",
84+
"var x = {|#0:OtherClass.TestingAndPrivateProperty|};",
85+
"{|#0:OtherClass.TestingAndPrivateProperty|} = string.Empty;",
86+
"var x = {|#0:OtherClass.TestingAndPrivateMethod()|};",
87+
"var x = {|#0:new OtherClass()|};",
88+
};
89+
6890
public static IEnumerable<object[]> NonPublicCombination { get; } =
6991
from attrs in GetTestingOnlyAttributes
7092
from includeNamespace in new[] { true, false }
@@ -86,6 +108,20 @@ from api in ShouldThrowButDoesnt
86108
from conditional in new[] { true, false }
87109
select new object[] { attrs, includeNamespace, api, conditional };
88110

111+
public static IEnumerable<object[]> TestingAndPrivateOnlySameTypeCombination { get; } =
112+
from attrs in GetTestingAndPrivateOnlyAttributes
113+
from includeNamespace in new[] { true, false }
114+
from api in TestingAndPrivateOnlySameTypeAccesses
115+
from conditional in new[] { true, false }
116+
select new object[] { attrs, includeNamespace, api, conditional };
117+
118+
public static IEnumerable<object[]> TestingAndPrivateOnlyDifferentTypeCombination { get; } =
119+
from attrs in GetTestingAndPrivateOnlyAttributes
120+
from includeNamespace in new[] { true, false }
121+
from api in TestingAndPrivateOnlyDifferentTypeAccesses
122+
from conditional in new[] { true, false }
123+
select new object[] { attrs, includeNamespace, api, conditional };
124+
89125
[Fact]
90126
public async Task EmptySourceShouldNotHaveDiagnostics()
91127
{
@@ -133,6 +169,27 @@ public async Task NotSupported(string publicAttribute, bool includeNamespace, st
133169
await ideallyShouldThrow.Should().ThrowAsync<InvalidOperationException>();
134170
}
135171

172+
[Theory]
173+
[MemberData(nameof(TestingAndPrivateOnlySameTypeCombination))]
174+
public async Task ShouldNotFlagTestingAndPrivateOnlyWhenCalledFromSameType(string publicAttribute, bool includeNamespace, string testFragment, bool attributeIsConditional)
175+
{
176+
var code = GetSampleCode(publicAttribute, includeNamespace, testFragment, attributeIsConditional);
177+
178+
// No diagnostics expected - calls from within the same type should be allowed
179+
await Verifier.VerifyAnalyzerAsync(code);
180+
}
181+
182+
[Theory]
183+
[MemberData(nameof(TestingAndPrivateOnlyDifferentTypeCombination))]
184+
public async Task ShouldFlagTestingAndPrivateOnlyWhenCalledFromDifferentType(string publicAttribute, bool includeNamespace, string testFragment, bool attributeIsConditional)
185+
{
186+
var code = GetSampleCode(publicAttribute, includeNamespace, testFragment, attributeIsConditional);
187+
188+
var expected = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error)
189+
.WithLocation(0);
190+
await Verifier.VerifyAnalyzerAsync(code, expected);
191+
}
192+
136193
private static string GetSampleCode(string publicAttribute, bool includeNamespace, string testFragment, bool attributeIsConditional)
137194
{
138195
var attributePrefix = includeNamespace ? "ConsoleApplication1." : string.Empty;
@@ -147,7 +204,7 @@ private static string GetSampleCode(string publicAttribute, bool includeNamespac
147204
using System.Diagnostics;
148205
149206
namespace ConsoleApplication1;
150-
207+
151208
{{GetAttributeDefinition(publicAttribute, attributeIsConditional)}}
152209
153210
class TestClass
@@ -161,7 +218,7 @@ internal TestClass(string arg1) { }
161218
162219
[{{attributePrefix}}{{publicAttribute}}]
163220
public TestClass() { }
164-
221+
165222
public string NonPublicProperty { get; set; }
166223
167224
[{{attributePrefix}}{{publicAttribute}}]
@@ -211,6 +268,22 @@ public PublicClass() { }
211268
public static string PublicMethod() => "test";
212269
}
213270
271+
// Class for testing different-type access with TestingAndPrivateOnly
272+
class OtherClass
273+
{
274+
[{{attributePrefix}}{{publicAttribute}}]
275+
public static string _testingAndPrivateField;
276+
277+
[{{attributePrefix}}{{publicAttribute}}]
278+
public OtherClass() { }
279+
280+
[{{attributePrefix}}{{publicAttribute}}]
281+
public static string TestingAndPrivateProperty { get; set; }
282+
283+
[{{attributePrefix}}{{publicAttribute}}]
284+
public static string TestingAndPrivateMethod() => "test";
285+
}
286+
214287
interface IPublic
215288
{
216289
string PublicMethod();

0 commit comments

Comments
 (0)