Skip to content

Commit c739b02

Browse files
committed
feat: add analyzer for incorrect usage of NoAutoInclude
1 parent 86eae3d commit c739b02

4 files changed

Lines changed: 158 additions & 1 deletion

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using IntelliTect.Coalesce.Analyzer.Analyzers;
2+
3+
namespace IntelliTect.Coalesce.Analyzer.Tests;
4+
5+
public class Coalesce0014_NoAutoIncludeOnNonObjectPropertyTests : CSharpAnalyzerVerifier<AttributeUsageAnalyzer>
6+
{
7+
[Theory]
8+
[InlineData("string")]
9+
[InlineData("int")]
10+
[InlineData("int?")]
11+
[InlineData("DateTime")]
12+
[InlineData("DateOnly")]
13+
[InlineData("Guid")]
14+
[InlineData("byte[]")]
15+
[InlineData("bool")]
16+
[InlineData("decimal")]
17+
public async Task NoAutoInclude_OnNonObjectProperty_ReportsWarning(string propertyType)
18+
{
19+
await VerifyAnalyzerAsync($$"""
20+
public class TestClass
21+
{
22+
[{|COA0014:Read(NoAutoInclude = true)|}]
23+
public {{propertyType}} TestProperty { get; set; }
24+
}
25+
""");
26+
}
27+
28+
[Fact]
29+
public async Task NoAutoInclude_OnEnumProperty_ReportsWarning()
30+
{
31+
await VerifyAnalyzerAsync("""
32+
public enum TestEnum { A, B, C }
33+
34+
public class TestClass
35+
{
36+
[{|COA0014:Read(NoAutoInclude = true)|}]
37+
public TestEnum TestProperty { get; set; }
38+
}
39+
""");
40+
}
41+
42+
[Theory]
43+
[InlineData("RelatedClass")]
44+
[InlineData("List<RelatedClass>")]
45+
[InlineData("ICollection<RelatedClass>")]
46+
[InlineData("IEnumerable<RelatedClass>")]
47+
[InlineData("RelatedClass[]")]
48+
[InlineData("IRelatedClass")]
49+
public async Task NoAutoInclude_OnObjectOrCollectionProperty_NoWarning(string propertyType)
50+
{
51+
await VerifyAnalyzerAsync($$"""
52+
public interface IRelatedClass { int Id { get; } }
53+
public class RelatedClass { public int Id { get; set; } }
54+
55+
public class TestClass
56+
{
57+
[Read(NoAutoInclude = true)]
58+
public {{propertyType}} TestProperty { get; set; }
59+
}
60+
""");
61+
}
62+
63+
[Fact]
64+
public async Task NoAutoInclude_False_NoWarning()
65+
{
66+
await VerifyAnalyzerAsync("""
67+
public class TestClass
68+
{
69+
[Read(NoAutoInclude = false)]
70+
public string TestProperty { get; set; }
71+
}
72+
""");
73+
}
74+
75+
[Fact]
76+
public async Task NoAutoInclude_NotSpecified_NoWarning()
77+
{
78+
await VerifyAnalyzerAsync("""
79+
public class TestClass
80+
{
81+
[Read("Admin")]
82+
public string TestProperty { get; set; }
83+
}
84+
""");
85+
}
86+
87+
[Fact]
88+
public async Task NoAutoInclude_OnClass_NoWarning()
89+
{
90+
await VerifyAnalyzerAsync("""
91+
[Read(NoAutoInclude = true)]
92+
public class TestClass
93+
{
94+
public string TestProperty { get; set; }
95+
}
96+
""");
97+
}
98+
}

src/IntelliTect.Coalesce.Analyzer/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ COA0010 | Usage | Error | Save-related methods should not be overridden when the
2020
COA0011 | Usage | Error | Delete-related methods should not be overridden when the containing model has Delete attribute set to DenyAll, as these methods will never be called.
2121
COA0012 | Usage | Warning | Ordering operations (OrderBy, OrderByDescending, ThenBy, ThenByDescending) applied to queries returned from GetQuery methods may be overridden by client-specified sorting. Consider moving the ordering logic to ApplyListDefaultSorting or using [DefaultOrderBy] attributes on model properties.
2222
COA0013 | Usage | Error | Types can only have one of the following: [Service], [StandaloneEntity], [SimpleModel] attributes, or inherit from DbContext, IDataSource<T>, IBehaviors<T>, or IClassDto<T>.
23+
COA0014 | Usage | Warning | NoAutoInclude only affects navigation properties (objects or collections). It has no effect on simple data properties like strings, numbers, or dates.
2324
COA0201 | Usage | Info | IFile parameters on Coalesce-exposed methods should specify suggested file types using the [FileType] attribute to improve default user experience.
2425
COA1001 | Style | Info | ItemResult and ItemResult<T> constructors can often be replaced with implicit conversions from boolean, string, and object values. This provides cleaner, more readable code while maintaining the same functionality.
2526
COA1002 | Style | Hidden | Marks the unnecessary parts of ItemResult constructor calls that can be removed when using implicit conversions. This diagnostic helps IDE syntax highlighting identify which portions of the code will be simplified by the COA1001 code fix.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
; Unshipped analyzer release
22
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
33

4+
; NOTE: Coalesce does not do analyzer release tracking. All analyzers should be listed in .Shipped.md.
5+
46
### New Rules

src/IntelliTect.Coalesce.Analyzer/Analyzers/AttributeUsageAnalyzer.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,25 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer
8686
isEnabledByDefault: true,
8787
description: "Types can only have one of the following: [Service], [StandaloneEntity], [SimpleModel] attributes, or inherit from DbContext, IDataSource<T>, IBehaviors<T>, or IClassDto<T>.");
8888

89-
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(InvalidInjectAttributeUsageRule, InvalidCoalesceUsageOnNestedTypesRule, InvalidCoalesceUsageRule, UnexposedSecondaryAttributeForTypesRule, UnexposedSecondaryAttributeForMethodsRule, MissingFileTypeAttributeRule, InvalidSemanticKernelAttributeUsageRule, GenericInvalidAttributeUsageRule, MutuallyExclusiveCoalesceTypesRule);
89+
public static readonly DiagnosticDescriptor NoAutoIncludeOnNonObjectPropertyRule = new(
90+
id: "COA0014",
91+
title: "NoAutoInclude has no effect on non-object properties",
92+
messageFormat: "NoAutoInclude has no effect on non-object properties",
93+
category: "Usage",
94+
defaultSeverity: DiagnosticSeverity.Warning,
95+
isEnabledByDefault: true,
96+
customTags: [WellKnownDiagnosticTags.Unnecessary],
97+
description: "NoAutoInclude only affects navigation properties (objects or collections). It has no effect on simple data properties like strings, numbers, or dates.");
98+
99+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(InvalidInjectAttributeUsageRule, InvalidCoalesceUsageOnNestedTypesRule, InvalidCoalesceUsageRule, UnexposedSecondaryAttributeForTypesRule, UnexposedSecondaryAttributeForMethodsRule, MissingFileTypeAttributeRule, InvalidSemanticKernelAttributeUsageRule, GenericInvalidAttributeUsageRule, MutuallyExclusiveCoalesceTypesRule, NoAutoIncludeOnNonObjectPropertyRule);
90100

91101
public override void Initialize(AnalysisContext context)
92102
{
93103
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
94104
context.EnableConcurrentExecution();
95105
context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
96106
context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
107+
context.RegisterSymbolAction(AnalyzeProperty, SymbolKind.Property);
97108
}
98109

99110
private static void AnalyzeNamedType(SymbolAnalysisContext context)
@@ -286,4 +297,49 @@ private static bool IsValidCoalesceMethod(IMethodSymbol methodSymbol, bool inclu
286297

287298
return false;
288299
}
300+
301+
private static void AnalyzeProperty(SymbolAnalysisContext context)
302+
{
303+
var propertySymbol = (IPropertySymbol)context.Symbol;
304+
305+
// COA0014: Check for NoAutoInclude on non-object properties
306+
var readAttr = propertySymbol.GetAttributeByName("IntelliTect.Coalesce.DataAnnotations.ReadAttribute");
307+
if (readAttr is not null)
308+
{
309+
var noAutoIncludeArg = readAttr.NamedArguments.FirstOrDefault(arg => arg.Key == "NoAutoInclude");
310+
if (noAutoIncludeArg.Key is not null && noAutoIncludeArg.Value.Value is true && !IsPOCO(propertySymbol.Type))
311+
{
312+
var location = readAttr.GetLocation() ?? propertySymbol.Locations[0];
313+
context.ReportDiagnostic(Diagnostic.Create(NoAutoIncludeOnNonObjectPropertyRule, location));
314+
}
315+
}
316+
}
317+
318+
private static bool IsPOCO(ITypeSymbol type)
319+
{
320+
// Unwrap nullable types
321+
if (type is INamedTypeSymbol namedType && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
322+
{
323+
type = namedType.TypeArguments[0];
324+
}
325+
326+
// String is not an object type for our purposes
327+
if (type.SpecialType == SpecialType.System_String)
328+
return false;
329+
330+
// byte[] is not a collection type for our purposes (it's typically binary data)
331+
if (type is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
332+
return false;
333+
334+
// Arrays of non-byte types are collections
335+
if (type is IArrayTypeSymbol)
336+
return true;
337+
338+
// Class or interface types (excluding string which we already checked)
339+
// These are "object-like" types that could be navigation properties
340+
if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Interface)
341+
return true;
342+
343+
return false;
344+
}
289345
}

0 commit comments

Comments
 (0)