Skip to content

Commit 06c4cac

Browse files
authored
Refactor attribute-based suppressors and support Odin serialization attribute (#399)
* Refactor attribute-based suppressors and support Odin serialization attribute * Fix documentation * Fix documentation again * Update index * Typo
1 parent 8885a92 commit 06c4cac

19 files changed

+232
-206
lines changed

doc/USP0004.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# USP0004 Don't set fields with SerializeField or SerializeReference attributes to read-only
1+
# USP0004 Don't flag fields decorated with serialization attributes (like `SerializeField`, `SerializeReference` or `OdinSerialize`) as read-only
22

3-
Fields with the `SerializeField` or `SerializeReference` attributes should not be marked read-only.
3+
Fields with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes should not be marked read-only.
44

55
## Suppressed Diagnostic ID
66

@@ -23,4 +23,4 @@ The IDE does not detect that the field is ever assigned outside of the declarati
2323

2424
## Why do we suppress this diagnostic?
2525

26-
A field with the `SerializeField` or `SerializeReference` attributes are exposed and can be assigned in the Unity Inspector. Making the field read-only would break this behavior.
26+
A field with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes are exposed and can be assigned in the Unity Inspector. Making the field read-only would break this behavior.

doc/USP0006.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# USP0006 Don't flag private fields with SerializeField or SerializeReference attributes as unused
1+
# USP0006 Don't flag private fields decorated with serialization attributes (like `SerializeField`, `SerializeReference` or `OdinSerialize`) as unused
22

3-
Private fields with the `SerializeField` or `SerializeReference` attributes should not be marked as unused.
3+
Private fields with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes should not be marked as unused.
44

55
## Suppressed Diagnostic ID
66

@@ -23,4 +23,4 @@ The IDE does not detect that the field is ever used within the project. Therefor
2323

2424
## Why do we suppress this diagnostic?
2525

26-
A field with the `SerializeField` or `SerializeReference` attributes are exposed and can be used in the Unity Inspector.
26+
A field with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes are exposed and can be used in the Unity Inspector.

doc/USP0007.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# USP0007 Don't flag fields with SerializeField or SerializeReference attributes as never assigned
1+
# USP0007 Don't flag fields decorated with serialization attributes (like `SerializeField`, `SerializeReference` or `OdinSerialize`) as never assigned
22

3-
Fields with the `SerializeField` or `SerializeReference` attributes should not be marked as unassigned.
3+
Fields with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes should not be marked as unassigned.
44

55
## Suppressed Diagnostic ID
66

@@ -23,4 +23,4 @@ The compiler detected an uninitialized private field declaration that is never a
2323

2424
## Why do we suppress this diagnostic?
2525

26-
A field with the `SerializeField` or `SerializeReference` attributes are exposed to Unity and can be assigned in the Unity Inspector.
26+
A field with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes are exposed to Unity and can be assigned in the Unity Inspector.

doc/USP0009.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# USP0009 Don't flag methods with the ContextMenu attribute or referenced by a field with the ContextMenuItem attribute as unused
1+
# USP0009 Don't flag methods decorated with the ContextMenu attribute or referenced by a field with the ContextMenuItem attribute as unused
22

33
Methods decorated with `ContextMenu`/`MenuItem` attribute, or referenced by a field with `ContextMenuItem` are not unused.
44

doc/USP0010.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# USP0010 Don't flag fields with the ContextMenuItem attribute as unused
1+
# USP0010 Don't flag fields decorated with the ContextMenuItem attribute as unused
22

33
Fields decorated with the `ContextMenuItem` attribute are not unused.
44

doc/USP0011.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# USP0011 Don't make fields with the ContextMenuItem attribute read-only
1+
# USP0011 Don't make fields decorated with the ContextMenuItem attribute read-only
22

33
Fields decorated with the `ContextMenuItem` attribute should not be made read-only.
44

doc/USP0013.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# USP0013 Don't flag private fields with SerializeField or SerializeReference attributes as unused
1+
# USP0013 Don't flag fields decorated with serialization attributes (like `SerializeField`, `SerializeReference` or `OdinSerialize`) as unused
22

3-
Private fields with the `SerializeField` or `SerializeReference` attributes should not be marked as unused by FxCop.
3+
Private fields with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes should not be marked as unused by FxCop.
44

55
## Suppressed Diagnostic ID
66

@@ -23,4 +23,4 @@ FxCop does not detect that the field is ever used within the project. Therefore,
2323

2424
## Why do we suppress this diagnostic?
2525

26-
A field with the `SerializeField` or `SerializeReference` attributes are exposed and can be used in the Unity Inspector.
26+
A field with the `SerializeField`, `SerializeReference` or `OdinSerialize` attributes are exposed and can be used in the Unity Inspector.

doc/USP0019.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# USP0019 Don't flag private methods decorated with PreserveAttribute or UsedImplicitlyAttribute as unused
1+
# USP0019 Don't flag private members decorated with implicit-usage attributes as unused
22

3-
Methods decorated with `PreserveAttribute` or `UsedImplicitlyAttribute` attributes are not unused.
3+
Members decorated with `PreserveAttribute`, `UsedImplicitlyAttribute` or `CreatePropertyAttribute` attributes are not unused.
44

55
## Suppressed Diagnostic ID
66

doc/index.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,22 @@ ID | Suppressed ID | Justification
4848
[USP0001](USP0001.md) | IDE0029 | Unity objects should not use null coalescing
4949
[USP0002](USP0002.md) | IDE0031 | Unity objects should not use null propagation
5050
[USP0003](USP0003.md) | IDE0051 | The Unity runtime invokes Unity messages
51-
[USP0004](USP0004.md) | IDE0044 | Don't set fields with `SerializeField` or `SerializeReference` attributes to read-only
51+
[USP0004](USP0004.md) | IDE0044 | Don't flag fields decorated with serialization attributes as read-only
5252
[USP0005](USP0005.md) | IDE0060 | The Unity runtime invokes Unity messages
53-
[USP0006](USP0006.md) | IDE0051 | Don't flag private fields with `SerializeField` or `SerializeReference` attributes as unused
54-
[USP0007](USP0007.md) | CS0649 | Don't flag fields with `SerializeField` or `SerializeReference` attributes as never assigned
53+
[USP0006](USP0006.md) | IDE0051 | Don't flag private fields decorated with serialization attributes as unused
54+
[USP0007](USP0007.md) | CS0649 | Don't flag fields decorated with serialization attributes as never assigned
5555
[USP0008](USP0008.md) | IDE0051 | Don't flag private methods used with `Invoke`/`InvokeRepeating` or `StartCoroutine`/`StopCoroutine` as unused
56-
[USP0009](USP0009.md) | IDE0051 | Don't flag methods with `MenuItem`/`ContextMenu` attribute or referenced by a field with the `ContextMenuItem` attribute as unused
57-
[USP0010](USP0010.md) | IDE0051 | Don't flag fields with the `ContextMenuItem` attribute as unused
58-
[USP0011](USP0011.md) | IDE0044 | Don't make fields with the `ContextMenuItem` attribute read-only
56+
[USP0009](USP0009.md) | IDE0051 | Don't flag methods decorated with `MenuItem`/`ContextMenu` attribute or referenced by a field with the `ContextMenuItem` attribute as unused
57+
[USP0010](USP0010.md) | IDE0051 | Don't flag fields decorated with the `ContextMenuItem` attribute as unused
58+
[USP0011](USP0011.md) | IDE0044 | Don't make fields decorated with the `ContextMenuItem` attribute read-only
5959
[USP0012](USP0012.md) | IDE0051 | Don't flag private methods with `InitializeOnLoadMethod`, `RuntimeInitializeOnLoadMethod` or `DidReloadScripts` attribute as unused
60-
[USP0013](USP0013.md) | CA1823 | Don't flag private fields with `SerializeField` or `SerializeReference` attributes as unused
60+
[USP0013](USP0013.md) | CA1823 | Don't flag private fields decorated with serialization attributes as unused
6161
[USP0014](USP0014.md) | CA1822 | The Unity runtime invokes Unity messages
6262
[USP0015](USP0015.md) | CA1801 | The Unity runtime invokes Unity messages
6363
[USP0016](USP0016.md) | CS8618 | Initialization detection with nullable reference types
6464
[USP0017](USP0017.md) | IDE0074 | Unity objects should not use coalescing assignment
6565
[USP0018](USP0018.md) | IDE0016 | Unity objects should not be used with throw expressions
66-
[USP0019](USP0012.md) | IDE0051 | Don't flag private methods with `PreserveAttribute` or `UsedImplicitlyAttribute` as unused
66+
[USP0019](USP0019.md) | IDE0051 | Don't flag private members with implicit-usage attributes as unused
6767
[USP0020](USP0020.md) | IDE0052 | The Unity runtime invokes Unity messages
6868
[USP0021](USP0021.md) | IDE0041 | Prefer reference equality
6969
[USP0022](USP0022.md) | IDE0270 | Unity objects should not use if null coalescing

src/Microsoft.Unity.Analyzers.Tests/ImplicitUsageAttributeSuppressorTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ namespace Microsoft.Unity.Analyzers.Tests;
1010

1111
public class ImplicitUsageAttributeSuppressorTests : BaseSuppressorVerifierTest<ImplicitUsageAttributeSuppressor>
1212
{
13+
[Fact]
14+
public async Task PrivateFieldWithCreateAttributeUnusedSuppressed()
15+
{
16+
const string test = @"
17+
using UnityEngine;
18+
19+
class Camera : MonoBehaviour
20+
{
21+
[Unity.Properties.CreateProperty]
22+
string someField = ""default"";
23+
}
24+
";
25+
var context = AnalyzerVerificationContext.Default
26+
.WithAnalyzerOption("dotnet_style_readonly_field", "false");
27+
28+
var suppressor = ExpectSuppressor(ImplicitUsageAttributeSuppressor.Rule)
29+
.WithLocation(7, 12);
30+
31+
await VerifyCSharpDiagnosticAsync(context, test, suppressor);
32+
}
33+
1334
[Fact]
1435
public async Task UnityPreserveTest()
1536
{

src/Microsoft.Unity.Analyzers.Tests/SerializeFieldCodeQualitySuppressorTests.cs renamed to src/Microsoft.Unity.Analyzers.Tests/SerializedFieldCodeQualitySuppressorTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace Microsoft.Unity.Analyzers.Tests;
1010

11-
public class SerializeFieldCodeQualitySuppressorTests : BaseSuppressorVerifierTest<SerializeFieldSuppressor>
11+
public class SerializedFieldCodeQualitySuppressorTests : BaseSuppressorVerifierTest<SerializedFieldSuppressor>
1212
{
1313
// Only load CodeQuality analyzers for those tests
1414
protected override SuppressorVerifierAnalyzers SuppressorVerifierAnalyzers => SuppressorVerifierAnalyzers.CodeQuality;
@@ -26,7 +26,7 @@ class Camera : MonoBehaviour
2626
}
2727
";
2828

29-
var suppressor = ExpectSuppressor(SerializeFieldSuppressor.UnusedCodeQualityRule)
29+
var suppressor = ExpectSuppressor(SerializedFieldSuppressor.UnusedCodeQualityRule)
3030
.WithLocation(7, 12);
3131

3232
await VerifyCSharpDiagnosticAsync(test, suppressor);

src/Microsoft.Unity.Analyzers.Tests/SerializeFieldSuppressorTests.cs renamed to src/Microsoft.Unity.Analyzers.Tests/SerializedFieldSuppressorTests.cs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Microsoft.Unity.Analyzers.Tests;
1111

12-
public class SerializeFieldSuppressorTests : BaseSuppressorVerifierTest<SerializeFieldSuppressor>
12+
public class SerializedFieldSuppressorTests : BaseSuppressorVerifierTest<SerializedFieldSuppressor>
1313
{
1414
[Fact]
1515
public async Task PrivateFieldWithAttributeNeverAssignedSuppressed()
@@ -31,7 +31,7 @@ private void ReadField() {
3131
.WithAnalyzerOption("dotnet_style_readonly_field", "false")
3232
.WithAnalyzerFilter("IDE0051");
3333

34-
var suppressor = ExpectSuppressor(SerializeFieldSuppressor.NeverAssignedRule)
34+
var suppressor = ExpectSuppressor(SerializedFieldSuppressor.NeverAssignedRule)
3535
.WithLocation(7, 12);
3636

3737
await VerifyCSharpDiagnosticAsync(context, test, suppressor);
@@ -56,7 +56,7 @@ private void ReadField() {
5656
var context = AnalyzerVerificationContext.Default
5757
.WithAnalyzerFilter("IDE0051");
5858

59-
var suppressor = ExpectSuppressor(SerializeFieldSuppressor.ReadonlyRule)
59+
var suppressor = ExpectSuppressor(SerializedFieldSuppressor.ReadonlyRule)
6060
.WithLocation(7, 20);
6161

6262
await VerifyCSharpDiagnosticAsync(context, test, suppressor);
@@ -74,7 +74,7 @@ class Camera : MonoBehaviour
7474
}
7575
";
7676

77-
var suppressor = ExpectSuppressor(SerializeFieldSuppressor.NeverAssignedRule)
77+
var suppressor = ExpectSuppressor(SerializedFieldSuppressor.NeverAssignedRule)
7878
.WithLocation(6, 19);
7979

8080
await VerifyCSharpDiagnosticAsync(test, suppressor);
@@ -91,7 +91,7 @@ class Test : System.Object
9191
";
9292

9393
// We don't want to suppress 'never assigned' for standard types
94-
var diagnostic = new DiagnosticResult(SerializeFieldSuppressor.NeverAssignedRule.SuppressedDiagnosticId, DiagnosticSeverity.Warning)
94+
var diagnostic = new DiagnosticResult(SerializedFieldSuppressor.NeverAssignedRule.SuppressedDiagnosticId, DiagnosticSeverity.Warning)
9595
.WithLocation(4, 19)
9696
.WithMessage("Field 'Test.someField' is never assigned to, and will always have its default value null");
9797

@@ -114,28 +114,7 @@ class Camera : MonoBehaviour
114114
var context = AnalyzerVerificationContext.Default
115115
.WithAnalyzerOption("dotnet_style_readonly_field", "false");
116116

117-
var suppressor = ExpectSuppressor(SerializeFieldSuppressor.UnusedRule)
118-
.WithLocation(7, 12);
119-
120-
await VerifyCSharpDiagnosticAsync(context, test, suppressor);
121-
}
122-
123-
[Fact]
124-
public async Task PrivateFieldWithCreateAttributeUnusedSuppressed()
125-
{
126-
const string test = @"
127-
using UnityEngine;
128-
129-
class Camera : MonoBehaviour
130-
{
131-
[Unity.Properties.CreateProperty]
132-
string someField = ""default"";
133-
}
134-
";
135-
var context = AnalyzerVerificationContext.Default
136-
.WithAnalyzerOption("dotnet_style_readonly_field", "false");
137-
138-
var suppressor = ExpectSuppressor(SerializeFieldSuppressor.UnusedRule)
117+
var suppressor = ExpectSuppressor(SerializedFieldSuppressor.UnusedRule)
139118
.WithLocation(7, 12);
140119

141120
await VerifyCSharpDiagnosticAsync(context, test, suppressor);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*--------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*-------------------------------------------------------------------------------------------*/
5+
6+
using System;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
12+
namespace Microsoft.Unity.Analyzers;
13+
14+
public abstract class BaseAttributeSuppressor : DiagnosticSuppressor
15+
{
16+
17+
protected abstract Type[] SuppressableAttributeTypes { get; }
18+
19+
public override void ReportSuppressions(SuppressionAnalysisContext context)
20+
{
21+
foreach (var diagnostic in context.ReportedDiagnostics)
22+
{
23+
AnalyzeDiagnostic(diagnostic, context);
24+
}
25+
}
26+
27+
private static bool IsSuppressableAttribute(INamedTypeSymbol? symbol, Type type)
28+
{
29+
return symbol != null && symbol.Matches(type);
30+
}
31+
32+
protected virtual bool IsSuppressableAttribute(INamedTypeSymbol? symbol)
33+
{
34+
return SuppressableAttributeTypes.Any(type => IsSuppressableAttribute(symbol, type));
35+
}
36+
37+
protected virtual bool IsSuppressable(ISymbol symbol)
38+
{
39+
return symbol.GetAttributes().Any(a => IsSuppressableAttribute(a.AttributeClass));
40+
}
41+
42+
protected abstract SyntaxNode? GetSuppressibleNode(Diagnostic diagnostic, SuppressionAnalysisContext context);
43+
44+
private void AnalyzeDiagnostic(Diagnostic diagnostic, SuppressionAnalysisContext context)
45+
{
46+
var node = GetSuppressibleNode(diagnostic, context);
47+
if (node == null)
48+
return;
49+
50+
var syntaxTree = diagnostic.Location.SourceTree;
51+
if (syntaxTree == null)
52+
return;
53+
54+
var model = context.GetSemanticModel(syntaxTree);
55+
var symbol = model.GetDeclaredSymbol(node);
56+
if (symbol == null)
57+
return;
58+
59+
if (!IsSuppressable(symbol))
60+
return;
61+
62+
foreach (var descriptor in SupportedSuppressions.Where(d => d.SuppressedDiagnosticId == diagnostic.Id))
63+
context.ReportSuppression(Suppression.Create(descriptor, diagnostic));
64+
}
65+
}

src/Microsoft.Unity.Analyzers/ImplicitUsageAttributeSuppressor.cs

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,42 @@
33
* Licensed under the MIT License. See LICENSE in the project root for license information.
44
*-------------------------------------------------------------------------------------------*/
55

6+
using System;
67
using System.Collections.Immutable;
7-
using System.Linq;
88
using Microsoft.CodeAnalysis;
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using Microsoft.Unity.Analyzers.Resources;
12+
using Unity.Properties;
1213

1314
namespace Microsoft.Unity.Analyzers;
1415

1516
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16-
public class ImplicitUsageAttributeSuppressor : DiagnosticSuppressor
17+
public class ImplicitUsageAttributeSuppressor : BaseAttributeSuppressor
1718
{
1819
internal static readonly SuppressionDescriptor Rule = new(
1920
id: "USP0019",
2021
suppressedDiagnosticId: "IDE0051",
2122
justification: Strings.ImplicitUsageAttributeSuppressorJustification);
2223

23-
public override void ReportSuppressions(SuppressionAnalysisContext context)
24-
{
25-
foreach (var diagnostic in context.ReportedDiagnostics)
26-
{
27-
AnalyzeDiagnostic(diagnostic, context);
28-
}
29-
}
24+
protected override Type[] SuppressableAttributeTypes =>
25+
[
26+
typeof(JetBrains.Annotations.UsedImplicitlyAttribute),
27+
typeof(UnityEngine.Scripting.PreserveAttribute),
28+
typeof(CreatePropertyAttribute)
29+
];
3030

3131
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [Rule];
3232

33-
private static void AnalyzeDiagnostic(Diagnostic diagnostic, SuppressionAnalysisContext context)
33+
protected override SyntaxNode? GetSuppressibleNode(Diagnostic diagnostic, SuppressionAnalysisContext context)
3434
{
35-
var methodDeclarationSyntax = context.GetSuppressibleNode<MethodDeclarationSyntax>(diagnostic);
36-
if (methodDeclarationSyntax == null)
37-
return;
38-
39-
var syntaxTree = diagnostic.Location.SourceTree;
40-
if (syntaxTree == null)
41-
return;
42-
43-
var model = context.GetSemanticModel(syntaxTree);
44-
if (model.GetDeclaredSymbol(methodDeclarationSyntax) is not IMethodSymbol methodSymbol)
45-
return;
46-
47-
if (!IsSuppressable(methodSymbol))
48-
return;
49-
50-
context.ReportSuppression(Suppression.Create(Rule, diagnostic));
35+
return context.GetSuppressibleNode<MemberDeclarationSyntax>(diagnostic) as SyntaxNode
36+
?? context.GetSuppressibleNode<VariableDeclaratorSyntax>(diagnostic);
5137
}
5238

53-
private static bool IsSuppressable(IMethodSymbol methodSymbol)
39+
protected override bool IsSuppressableAttribute(INamedTypeSymbol? symbol)
5440
{
5541
// The Unity code stripper will consider any attribute with the exact name "PreserveAttribute", regardless of the namespace or assembly
56-
return methodSymbol.GetAttributes().Any(a => a.AttributeClass != null && (a.AttributeClass.Matches(typeof(JetBrains.Annotations.UsedImplicitlyAttribute)) || a.AttributeClass.Name == nameof(UnityEngine.Scripting.PreserveAttribute)));
42+
return base.IsSuppressableAttribute(symbol) || symbol is { Name: nameof(UnityEngine.Scripting.PreserveAttribute) };
5743
}
5844
}

0 commit comments

Comments
 (0)