diff --git a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs index 2ecb89efd..48c60b333 100755 --- a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs +++ b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs @@ -244,6 +244,66 @@ public static bool IsNumberType(this ITypeSymbol? symbol) } } + /// + /// Determines whether the type is a blittable type. + /// Blittable types: byte, sbyte, short, ushort, int, uint, long, ulong, + /// float, double, IntPtr, UIntPtr, pointers, enums, and structs + /// containing only blittable fields. + /// + public static bool IsBlittableType(this ITypeSymbol? symbol) + { + if (symbol is null) + return false; + + switch (symbol.SpecialType) + { + case SpecialType.System_Byte: + case SpecialType.System_SByte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_IntPtr: + case SpecialType.System_UIntPtr: + return true; + + case SpecialType.None: + break; + + default: + return false; + } + + if (symbol.TypeKind is TypeKind.Pointer) + return true; + + if (symbol is INamedTypeSymbol namedType) + { + if (namedType.EnumUnderlyingType is not null) + return true; + + if (namedType.IsValueType) + { + foreach (var member in namedType.GetMembers()) + { + if (member is not IFieldSymbol field || field.IsConst || field.IsStatic) + continue; + + if (!field.Type.IsBlittableType()) + return false; + } + + return true; + } + } + + return false; + } + public static bool IsUnitTestClass(this ITypeSymbol typeSymbol) { var attributes = typeSymbol.GetAttributes(); diff --git a/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs index b12de10c8..02c7ad91e 100644 --- a/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseStructLayoutAttributeAnalyzer.cs @@ -54,8 +54,8 @@ private static void Analyze(SymbolAnalysisContext context) if (member.IsConst || member.IsStatic) continue; - if (member.Type.IsReferenceType) - return; // When a struct contains a reference type field, the layout is automatically changed to Auto + if (!member.Type.IsBlittableType()) + return; memberCount++; } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseStructLayoutAttributeAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseStructLayoutAttributeAnalyzerTests.cs index 7ca95812e..78d6865a0 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseStructLayoutAttributeAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseStructLayoutAttributeAnalyzerTests.cs @@ -129,6 +129,165 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task WithBoolFields_ShouldNotReportDiagnostic() + { + const string SourceCode = """ + struct TypeName + { + bool a; + bool b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithCharFields_ShouldNotReportDiagnostic() + { + const string SourceCode = """ + struct TypeName + { + char a; + char b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithDecimalFields_ShouldNotReportDiagnostic() + { + const string SourceCode = """ + struct TypeName + { + decimal a; + decimal b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithIntPtrFields_ShouldReportDiagnostic() + { + const string SourceCode = """ + struct [|TypeName|] + { + nint a; + nint b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithUIntPtrFields_ShouldReportDiagnostic() + { + const string SourceCode = """ + struct [|TypeName|] + { + nuint a; + nuint b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithFloatAndDoubleFields_ShouldReportDiagnostic() + { + const string SourceCode = """ + struct [|TypeName|] + { + float a; + double b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithEnumFields_ShouldReportDiagnostic() + { + const string SourceCode = """ + enum MyEnum { A, B } + struct [|TypeName|] + { + MyEnum a; + MyEnum b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithBlittableNestedStruct_ShouldReportDiagnostic() + { + const string SourceCode = """ + struct Inner + { + int x; + } + struct [|TypeName|] + { + Inner a; + int b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithNonBlittableNestedStruct_ShouldNotReportDiagnostic() + { + const string SourceCode = """ + struct Inner + { + bool x; + } + struct TypeName + { + Inner a; + int b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task WithMixedBlittableAndNonBlittable_ShouldNotReportDiagnostic() + { + const string SourceCode = """ + struct TypeName + { + int a; + bool b; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + #if CSHARP10_OR_GREATER [Fact] public async Task RecordStruct()