diff --git a/.editorconfig b/.editorconfig index a53d2bc6..8ea80f5d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,9 @@ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true +[*] +insert_final_newline = true + # C# files [*.cs] @@ -13,7 +16,6 @@ tab_width = 4 # New line preferences end_of_line = crlf -insert_final_newline = true #### .NET Coding Conventions #### @@ -221,3 +223,8 @@ dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case +csharp_style_prefer_top_level_statements = true +csharp_style_prefer_primary_constructors = true + +# RCS1123: Add parentheses when necessary +dotnet_diagnostic.RCS1123.severity = none diff --git a/Directory.Packages.props b/Directory.Packages.props index e0e978ce..93c56b3c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,22 +1,22 @@ - - true - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EcoCode.sln b/EcoCode.sln index 3f343efa..a268a585 100644 --- a/EcoCode.sln +++ b/EcoCode.sln @@ -20,13 +20,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EcoCode.Vsix", "src\EcoCode EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{810939C8-2A48-4F56-84C3-E433CDB923EE}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml .github\workflows\create-tag-release.yml = .github\workflows\create-tag-release.yml Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props global.json = global.json icon.jpeg = icon.jpeg - ProductionCode.ruleset = ProductionCode.ruleset README.md = README.md SharedAssemblyInfo.cs = SharedAssemblyInfo.cs EndProjectSection diff --git a/README.md b/README.md index ccdb06b8..60faf01f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Both the EcoCode NuGet package and Visual Studio extension target .Net Standard |[EC69](https://github.com/green-code-initiative/ecoCode/blob/main/ecocode-rules-specifications/src/main/rules/EC69/csharp/EC69.asciidoc)|Don’t call loop invariant functions in loop conditions|⚠️|✔️|❌| |[EC72](https://github.com/green-code-initiative/ecoCode/blob/main/ecocode-rules-specifications/src/main/rules/EC72/csharp/EC72.asciidoc)|Don’t execute SQL queries in loops|⚠️|✔️|❌| |[EC75](https://github.com/green-code-initiative/ecoCode/blob/main/ecocode-rules-specifications/src/main/rules/EC75/csharp/EC75.asciidoc)|Don’t concatenate `strings` in loops|⚠️|✔️|❌| +|[EC81](https://github.com/green-code-initiative/ecoCode/blob/main/ecocode-rules-specifications/src/main/rules/EC81/csharp/EC81.asciidoc)|Specify struct layouts|⚠️|✔️|✔️| 🤝 Contribution --------------- diff --git a/SharedAssemblyInfo.cs b/SharedAssemblyInfo.cs index c5597e45..aa6eae15 100644 --- a/SharedAssemblyInfo.cs +++ b/SharedAssemblyInfo.cs @@ -2,4 +2,4 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: CLSCompliant(false)] \ No newline at end of file +[assembly: CLSCompliant(false)] diff --git a/src/EcoCode.Analyzers/Analyzers/DontCallFunctionsInLoopConditions.cs b/src/EcoCode.Analyzers/Analyzers/EC69_DontCallFunctionsInLoopConditionsAnalyzer.cs similarity index 94% rename from src/EcoCode.Analyzers/Analyzers/DontCallFunctionsInLoopConditions.cs rename to src/EcoCode.Analyzers/Analyzers/EC69_DontCallFunctionsInLoopConditionsAnalyzer.cs index f039b154..5beff39c 100644 --- a/src/EcoCode.Analyzers/Analyzers/DontCallFunctionsInLoopConditions.cs +++ b/src/EcoCode.Analyzers/Analyzers/EC69_DontCallFunctionsInLoopConditionsAnalyzer.cs @@ -2,7 +2,7 @@ /// Analyzer for don't call loop invariant functions in loop conditions. [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DontCallFunctionsInLoopConditions : DiagnosticAnalyzer +public sealed class DontCallFunctionsInLoopConditionsAnalyzer : DiagnosticAnalyzer { private static readonly ImmutableArray SyntaxKinds = [ SyntaxKind.ForStatement, @@ -54,11 +54,15 @@ private static void AnalyzeLoopNode(SyntaxNodeAnalysisContext context) _ = loopInvariantSymbols.Add(symbol); } } + if (loopInvariantSymbols.Count == 0) return; // Step 2: Remove the variables that are mutated in the loop body or the for loop incrementors RemoveMutatedSymbols(expression.DescendantNodes(), loopInvariantSymbols, context.SemanticModel); + if (loopInvariantSymbols.Count == 0) return; + foreach (var inc in incrementors) RemoveMutatedSymbols(inc.DescendantNodesAndSelf(), loopInvariantSymbols, context.SemanticModel); + if (loopInvariantSymbols.Count == 0) return; // Step 3: Identify conditions that are loop invariant foreach (var node in condition.DescendantNodes()) @@ -98,4 +102,4 @@ static void RemoveMutatedSymbols(IEnumerable nodes, HashSet } } } -} \ No newline at end of file +} diff --git a/src/EcoCode.Analyzers/Analyzers/DontExecuteSqlCommandsInLoops.cs b/src/EcoCode.Analyzers/Analyzers/EC72_DontExecuteSqlCommandsInLoopsAnalyzer.cs similarity index 95% rename from src/EcoCode.Analyzers/Analyzers/DontExecuteSqlCommandsInLoops.cs rename to src/EcoCode.Analyzers/Analyzers/EC72_DontExecuteSqlCommandsInLoopsAnalyzer.cs index ff7f2b6d..eba8f53b 100644 --- a/src/EcoCode.Analyzers/Analyzers/DontExecuteSqlCommandsInLoops.cs +++ b/src/EcoCode.Analyzers/Analyzers/EC72_DontExecuteSqlCommandsInLoopsAnalyzer.cs @@ -2,7 +2,7 @@ /// Analyzer for don't execute SQL commands in loops. [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DontExecuteSqlCommandsInLoops : DiagnosticAnalyzer +public sealed class DontExecuteSqlCommandsInLoopsAnalyzer : DiagnosticAnalyzer { /// The diagnostic descriptor. public static DiagnosticDescriptor Descriptor { get; } = new( @@ -40,4 +40,4 @@ symbol.Name is "ExecuteNonQuery" or "ExecuteScalar" or "ExecuteReader" && }, SyntaxKind.InvocationExpression); }); } -} \ No newline at end of file +} diff --git a/src/EcoCode.Analyzers/Analyzers/DontConcatenateStringsInLoops.cs b/src/EcoCode.Analyzers/Analyzers/EC75_DontConcatenateStringsInLoopsAnalyzer.cs similarity index 96% rename from src/EcoCode.Analyzers/Analyzers/DontConcatenateStringsInLoops.cs rename to src/EcoCode.Analyzers/Analyzers/EC75_DontConcatenateStringsInLoopsAnalyzer.cs index 8fe69675..ada5fce0 100644 --- a/src/EcoCode.Analyzers/Analyzers/DontConcatenateStringsInLoops.cs +++ b/src/EcoCode.Analyzers/Analyzers/EC75_DontConcatenateStringsInLoopsAnalyzer.cs @@ -2,7 +2,7 @@ /// Analyzer for don't concatenate strings in loops. [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class DontConcatenateStringsInLoops : DiagnosticAnalyzer +public sealed class DontConcatenateStringsInLoopsAnalyzer : DiagnosticAnalyzer { private static readonly ImmutableArray SyntaxKinds = [ SyntaxKind.ForStatement, @@ -48,4 +48,4 @@ assignment.Left is IdentifierNameSyntax identifierName && } } } -} \ No newline at end of file +} diff --git a/src/EcoCode.Analyzers/Analyzers/EC81_SpecifyStructLayoutAnalyzer.cs b/src/EcoCode.Analyzers/Analyzers/EC81_SpecifyStructLayoutAnalyzer.cs new file mode 100644 index 00000000..22f3ddf4 --- /dev/null +++ b/src/EcoCode.Analyzers/Analyzers/EC81_SpecifyStructLayoutAnalyzer.cs @@ -0,0 +1,59 @@ +namespace EcoCode.Analyzers; + +/// Analyzer for specify struct layout. +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class SpecifyStructLayoutAnalyzer : DiagnosticAnalyzer +{ + private static readonly ImmutableArray SymbolKinds = [SymbolKind.NamedType]; + + /// The diagnostic descriptor. + public static DiagnosticDescriptor Descriptor { get; } = new( + Rule.Ids.EC81_UseStructLayout, + title: "Use struct layout", + messageFormat: "Use struct layout", + Rule.Categories.Performance, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: string.Empty, + helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC81_UseStructLayout)); + + /// + public override ImmutableArray SupportedDiagnostics => [Descriptor]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(static context => Analyze(context), SymbolKinds); + } + + private static void Analyze(SymbolAnalysisContext context) + { + if (context.Symbol is not INamedTypeSymbol symbol || !symbol.IsValueType || symbol.EnumUnderlyingType is not null) + return; + + var structLayoutAttributeType = context.Compilation.GetBestTypeByMetadataName("System.Runtime.InteropServices.StructLayoutAttribute"); + if (structLayoutAttributeType is null) return; + + foreach (var attr in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(structLayoutAttributeType, attr.AttributeClass)) + return; + } + + int memberCount = 0; + foreach (var member in symbol.GetMembers()) + { + if (member is not IFieldSymbol fieldSymbol || fieldSymbol.IsConst || fieldSymbol.IsStatic) + continue; + + if (fieldSymbol.Type.IsReferenceType) return; // A struct containing a reference type is always in auto layout + memberCount++; + } + + if (memberCount < 2) return; + foreach (var location in symbol.Locations) + context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); + } +} diff --git a/src/EcoCode.Analyzers/EcoCode.Analyzers.csproj b/src/EcoCode.Analyzers/EcoCode.Analyzers.csproj index 3e89d50b..eed48b54 100644 --- a/src/EcoCode.Analyzers/EcoCode.Analyzers.csproj +++ b/src/EcoCode.Analyzers/EcoCode.Analyzers.csproj @@ -7,16 +7,9 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/EcoCode.Analyzers/Rule.cs b/src/EcoCode.Analyzers/Rule.cs index a085e1ca..525ea234 100644 --- a/src/EcoCode.Analyzers/Rule.cs +++ b/src/EcoCode.Analyzers/Rule.cs @@ -17,5 +17,6 @@ public static class Ids public const string EC69_DontCallFunctionsInLoopConditions = "EC69"; public const string EC72_DontExecuteSqlCommandsInLoops = "EC72"; public const string EC75_DontConcatenateStringsInLoops = "EC75"; + public const string EC81_UseStructLayout = "EC81"; } } diff --git a/src/EcoCode.CodeFixes/CodeFixes/DontCallFunctionsInLoopConditionsCodeFixProvider.cs b/src/EcoCode.CodeFixes/CodeFixes/EC69_DontCallFunctionsInLoopConditionsCodeFixProvider.cs similarity index 91% rename from src/EcoCode.CodeFixes/CodeFixes/DontCallFunctionsInLoopConditionsCodeFixProvider.cs rename to src/EcoCode.CodeFixes/CodeFixes/EC69_DontCallFunctionsInLoopConditionsCodeFixProvider.cs index 92435d85..e034878d 100644 --- a/src/EcoCode.CodeFixes/CodeFixes/DontCallFunctionsInLoopConditionsCodeFixProvider.cs +++ b/src/EcoCode.CodeFixes/CodeFixes/EC69_DontCallFunctionsInLoopConditionsCodeFixProvider.cs @@ -5,11 +5,11 @@ public sealed class DontCallFunctionsInLoopConditionsCodeFixProvider : CodeFixProvider { /// - public override ImmutableArray FixableDiagnosticIds => [DontCallFunctionsInLoopConditions.Descriptor.Id]; + public override ImmutableArray FixableDiagnosticIds => [DontCallFunctionsInLoopConditionsAnalyzer.Descriptor.Id]; /// public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask; -} \ No newline at end of file +} diff --git a/src/EcoCode.CodeFixes/CodeFixes/DontExecuteSqlCommandsInLoopsCodeFixProvider.cs b/src/EcoCode.CodeFixes/CodeFixes/EC72_DontExecuteSqlCommandsInLoopsCodeFixProvider.cs similarity index 91% rename from src/EcoCode.CodeFixes/CodeFixes/DontExecuteSqlCommandsInLoopsCodeFixProvider.cs rename to src/EcoCode.CodeFixes/CodeFixes/EC72_DontExecuteSqlCommandsInLoopsCodeFixProvider.cs index cfef66f9..c280cf1a 100644 --- a/src/EcoCode.CodeFixes/CodeFixes/DontExecuteSqlCommandsInLoopsCodeFixProvider.cs +++ b/src/EcoCode.CodeFixes/CodeFixes/EC72_DontExecuteSqlCommandsInLoopsCodeFixProvider.cs @@ -5,11 +5,11 @@ public sealed class DontExecuteSqlCommandsInLoopsCodeFixProvider : CodeFixProvider { /// - public override ImmutableArray FixableDiagnosticIds => [DontExecuteSqlCommandsInLoops.Descriptor.Id]; + public override ImmutableArray FixableDiagnosticIds => [DontExecuteSqlCommandsInLoopsAnalyzer.Descriptor.Id]; /// public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask; -} \ No newline at end of file +} diff --git a/src/EcoCode.CodeFixes/CodeFixes/DontConcatenateStringsInLoopsCodeFixProvider.cs b/src/EcoCode.CodeFixes/CodeFixes/EC75_DontConcatenateStringsInLoopsCodeFixProvider.cs similarity index 92% rename from src/EcoCode.CodeFixes/CodeFixes/DontConcatenateStringsInLoopsCodeFixProvider.cs rename to src/EcoCode.CodeFixes/CodeFixes/EC75_DontConcatenateStringsInLoopsCodeFixProvider.cs index ae4689ca..81fb020e 100644 --- a/src/EcoCode.CodeFixes/CodeFixes/DontConcatenateStringsInLoopsCodeFixProvider.cs +++ b/src/EcoCode.CodeFixes/CodeFixes/EC75_DontConcatenateStringsInLoopsCodeFixProvider.cs @@ -6,11 +6,11 @@ public sealed class DontConcatenateStringsInLoopsCodeFixProvider : CodeFixProvider { /// - public override ImmutableArray FixableDiagnosticIds => [DontConcatenateStringsInLoops.Descriptor.Id]; + public override ImmutableArray FixableDiagnosticIds => [DontConcatenateStringsInLoopsAnalyzer.Descriptor.Id]; /// public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask; -} \ No newline at end of file +} diff --git a/src/EcoCode.CodeFixes/CodeFixes/EC81_SpecifyStructLayoutCodeFixProvider.cs b/src/EcoCode.CodeFixes/CodeFixes/EC81_SpecifyStructLayoutCodeFixProvider.cs new file mode 100644 index 00000000..44d23299 --- /dev/null +++ b/src/EcoCode.CodeFixes/CodeFixes/EC81_SpecifyStructLayoutCodeFixProvider.cs @@ -0,0 +1,60 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; +using System.Threading; + +namespace EcoCode.CodeFixes; + +/// The code fix provider for use struct layout. +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SpecifyStructLayoutCodeFixProvider)), Shared] +public sealed class SpecifyStructLayoutCodeFixProvider : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds => [SpecifyStructLayoutAnalyzer.Descriptor.Id]; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (node is not TypeDeclarationSyntax nodeToFix) return; + + context.RegisterCodeFix( + CodeAction.Create( + "Add Auto StructLayout attribute", + ct => Refactor(context.Document, nodeToFix, LayoutKind.Auto, ct), + equivalenceKey: "Add Auto StructLayout attribute"), + context.Diagnostics); + + context.RegisterCodeFix( + CodeAction.Create( + "Add Sequential StructLayout attribute", + ct => Refactor(context.Document, nodeToFix, LayoutKind.Sequential, ct), + equivalenceKey: "Add Sequential StructLayout attribute"), + context.Diagnostics); + } + + private static async Task Refactor(Document document, SyntaxNode nodeToFix, LayoutKind layoutKind, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + var structLayoutAttributeType = editor.SemanticModel.Compilation.GetBestTypeByMetadataName("System.Runtime.InteropServices.StructLayoutAttribute"); + if (structLayoutAttributeType is null) return document; + + var layoutKindType = editor.SemanticModel.Compilation.GetBestTypeByMetadataName("System.Runtime.InteropServices.LayoutKind"); + if (layoutKindType is null) return document; + + editor.AddAttribute(nodeToFix, editor.Generator.Attribute( + editor.Generator.TypeExpression(structLayoutAttributeType).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation), + [ + editor.Generator.AttributeArgument(editor.Generator.MemberAccessExpression( + editor.Generator.TypeExpression(layoutKindType).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation), + layoutKind.ToString())), + ])); + return editor.GetChangedDocument(); + } +} diff --git a/src/EcoCode.CodeFixes/EcoCode.CodeFixes.csproj b/src/EcoCode.CodeFixes/EcoCode.CodeFixes.csproj index d1b1235d..e062bbf3 100644 --- a/src/EcoCode.CodeFixes/EcoCode.CodeFixes.csproj +++ b/src/EcoCode.CodeFixes/EcoCode.CodeFixes.csproj @@ -7,15 +7,9 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + @@ -23,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/EcoCode.CodeFixes/GlobalUsings.cs b/src/EcoCode.CodeFixes/GlobalUsings.cs index b05d6438..6bf54f4b 100644 --- a/src/EcoCode.CodeFixes/GlobalUsings.cs +++ b/src/EcoCode.CodeFixes/GlobalUsings.cs @@ -1,7 +1,8 @@ global using EcoCode.Analyzers; +global using EcoCode.Shared; global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.CodeFixes; global using System.Collections.Immutable; global using System.Composition; global using System.Runtime.InteropServices; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; diff --git a/src/EcoCode.Package/EcoCode.Package.csproj b/src/EcoCode.Package/EcoCode.Package.csproj index 5637b7d9..a9333921 100644 --- a/src/EcoCode.Package/EcoCode.Package.csproj +++ b/src/EcoCode.Package/EcoCode.Package.csproj @@ -45,17 +45,11 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + - + \ No newline at end of file diff --git a/src/EcoCode.Shared/EcoCode.Shared.csproj b/src/EcoCode.Shared/EcoCode.Shared.csproj index cbbe276d..b021e757 100644 --- a/src/EcoCode.Shared/EcoCode.Shared.csproj +++ b/src/EcoCode.Shared/EcoCode.Shared.csproj @@ -6,15 +6,9 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + \ No newline at end of file diff --git a/src/EcoCode.Shared/Extensions/CompilationExtensions.cs b/src/EcoCode.Shared/Extensions/CompilationExtensions.cs new file mode 100644 index 00000000..e187a181 --- /dev/null +++ b/src/EcoCode.Shared/Extensions/CompilationExtensions.cs @@ -0,0 +1,52 @@ +namespace EcoCode.Shared; + +/// Extension methods for . +public static class CompilationExtensions +{ + /// + /// Gets a type by its metadata name to use for code analysis within a . This method + /// attempts to find the "best" symbol to use for code analysis, which is the symbol matching the first of the + /// following rules. + /// + /// + /// + /// If only one type with the given name is found within the compilation and its referenced assemblies, that + /// type is returned regardless of accessibility. + /// + /// + /// If the current defines the symbol, that symbol is returned. + /// + /// + /// If exactly one referenced assembly defines the symbol in a manner that makes it visible to the current + /// , that symbol is returned. + /// + /// + /// Otherwise, this method returns . + /// + /// + /// + /// The to consider for analysis. + /// The fully-qualified metadata type name to find. + /// The symbol to use for code analysis; otherwise, . + /// Comes from Roslynator sourc code : + /// https://github.com/dotnet/roslyn/blob/d2ff1d83e8fde6165531ad83f0e5b1ae95908289/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/CompilationExtensions.cs#L11-L68 + /// + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string fullyQualifiedMetadataName) + { + var type = default(INamedTypeSymbol); + foreach (var currentType in compilation.GetTypesByMetadataName(fullyQualifiedMetadataName)) + { + if (ReferenceEquals(currentType.ContainingAssembly, compilation.Assembly)) + return currentType; + + var visibility = currentType.GetResultantVisibility(); + if (visibility is SymbolVisibility.Public || + visibility is SymbolVisibility.Internal && currentType.ContainingAssembly.GivesAccessTo(compilation.Assembly)) + { + if (type is object) return null; // Multiple visible types with the same metadata name are present + type = currentType; + } + } + return type; + } +} diff --git a/src/EcoCode.Shared/Extensions/SymbolExtensions.cs b/src/EcoCode.Shared/Extensions/SymbolExtensions.cs index 81187c97..87c257b1 100644 --- a/src/EcoCode.Shared/Extensions/SymbolExtensions.cs +++ b/src/EcoCode.Shared/Extensions/SymbolExtensions.cs @@ -79,4 +79,29 @@ public static bool IsDeclaredOutsideLoop(this ISymbol symbol, SyntaxNode loopNod } return null; } -} \ No newline at end of file + + /// Returns the visibility of a symbol. + /// The symbol. + /// The visibility of the symbol. + public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) + { + if (symbol.Kind is SymbolKind.Alias or SymbolKind.TypeParameter) + return SymbolVisibility.Private; + + if (symbol.Kind is SymbolKind.Parameter) // Parameters are only as visible as their containing symbol + return GetResultantVisibility(symbol.ContainingSymbol); + + var visibility = SymbolVisibility.Public; + while (symbol is not null && symbol.Kind is not SymbolKind.Namespace) + { + if (symbol.DeclaredAccessibility is Accessibility.NotApplicable or Accessibility.Private) + return SymbolVisibility.Private; + + if (symbol.DeclaredAccessibility is Accessibility.Internal or Accessibility.ProtectedAndInternal) + visibility = SymbolVisibility.Internal; + + symbol = symbol.ContainingSymbol; + } + return visibility; + } +} diff --git a/src/EcoCode.Shared/GlobalUsings.cs b/src/EcoCode.Shared/GlobalUsings.cs index 389fac6e..2e7d51b1 100644 --- a/src/EcoCode.Shared/GlobalUsings.cs +++ b/src/EcoCode.Shared/GlobalUsings.cs @@ -1,3 +1,3 @@ global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.CSharp.Syntax; -global using System.Linq; \ No newline at end of file +global using System.Linq; diff --git a/src/EcoCode.Shared/SymbolVisibility.cs b/src/EcoCode.Shared/SymbolVisibility.cs new file mode 100644 index 00000000..ffaadfac --- /dev/null +++ b/src/EcoCode.Shared/SymbolVisibility.cs @@ -0,0 +1,14 @@ +namespace EcoCode.Shared; + +/// The visibility of a symbol. +public enum SymbolVisibility +{ + /// Public visibility. + Public, + + /// Internal visibility. + Internal, + + /// Private visibility. + Private, +} diff --git a/src/EcoCode.Tests/EcoCode.Tests.csproj b/src/EcoCode.Tests/EcoCode.Tests.csproj index 84f41a33..b2f0d0c3 100644 --- a/src/EcoCode.Tests/EcoCode.Tests.csproj +++ b/src/EcoCode.Tests/EcoCode.Tests.csproj @@ -14,19 +14,15 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + + + diff --git a/src/EcoCode.Tests/LiveWarnings/DontCallFunctionsInLoopConditionsLiveWarnings.cs b/src/EcoCode.Tests/LiveWarnings/EC69_DontCallFunctionsInLoopConditionsLiveWarnings.cs similarity index 100% rename from src/EcoCode.Tests/LiveWarnings/DontCallFunctionsInLoopConditionsLiveWarnings.cs rename to src/EcoCode.Tests/LiveWarnings/EC69_DontCallFunctionsInLoopConditionsLiveWarnings.cs diff --git a/src/EcoCode.Tests/LiveWarnings/DontExecuteSqlCommandsInLoopsLiveWarnings.cs b/src/EcoCode.Tests/LiveWarnings/EC72_DontExecuteSqlCommandsInLoopsLiveWarnings.cs similarity index 100% rename from src/EcoCode.Tests/LiveWarnings/DontExecuteSqlCommandsInLoopsLiveWarnings.cs rename to src/EcoCode.Tests/LiveWarnings/EC72_DontExecuteSqlCommandsInLoopsLiveWarnings.cs diff --git a/src/EcoCode.Tests/LiveWarnings/DontConcatenateStringsInLoopsLiveWarnings.cs b/src/EcoCode.Tests/LiveWarnings/EC75_DontConcatenateStringsInLoopsLiveWarnings.cs similarity index 99% rename from src/EcoCode.Tests/LiveWarnings/DontConcatenateStringsInLoopsLiveWarnings.cs rename to src/EcoCode.Tests/LiveWarnings/EC75_DontConcatenateStringsInLoopsLiveWarnings.cs index b1e1f83a..2aa612af 100644 --- a/src/EcoCode.Tests/LiveWarnings/DontConcatenateStringsInLoopsLiveWarnings.cs +++ b/src/EcoCode.Tests/LiveWarnings/EC75_DontConcatenateStringsInLoopsLiveWarnings.cs @@ -32,4 +32,4 @@ public static void Run(string s0) s3 = i.ToString(provider: null); Console.WriteLine(s3); } -} \ No newline at end of file +} diff --git a/src/EcoCode.Tests/LiveWarnings/EC81_SpecifyStructLayoutLiveWarnings.cs b/src/EcoCode.Tests/LiveWarnings/EC81_SpecifyStructLayoutLiveWarnings.cs new file mode 100644 index 00000000..d539f624 --- /dev/null +++ b/src/EcoCode.Tests/LiveWarnings/EC81_SpecifyStructLayoutLiveWarnings.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; + +namespace EcoCode.Tests; + +internal static class SpecifyStructLayoutLiveWarnings +{ + private record struct TestStruct1; + + private record struct TestStruct2(int A); + + private record struct TestStruct3(string A); + + private record struct TestStruct4(int A, double B); // EC81 - Use struct layout + + [StructLayout(LayoutKind.Auto)] + private record struct TestStruct4_Fixed(int A, double B); + + private record struct TestStruct5(int A, string B); + + private record struct TestStruct6(int A, double B, int C); // EC81 - Use struct layout + + [StructLayout(LayoutKind.Auto)] + private record struct TestStruct6_Fixed(int A, double B, int C); + + private record struct TestStruct7(bool A, int B, char C, short D, ulong E, DateTime F); // EC81 - Use struct layout + + [StructLayout(LayoutKind.Auto)] + private record struct TestStruct7_Fixed(bool A, int B, char C, short D, ulong E, DateTime F); +} diff --git a/src/EcoCode.Tests/Tests/DontCallFunctionsInLoopConditionsUnitTests.cs b/src/EcoCode.Tests/Tests/EC69_DontCallFunctionsInLoopConditionsUnitTests.cs similarity index 98% rename from src/EcoCode.Tests/Tests/DontCallFunctionsInLoopConditionsUnitTests.cs rename to src/EcoCode.Tests/Tests/EC69_DontCallFunctionsInLoopConditionsUnitTests.cs index 16faad33..c3af233c 100644 --- a/src/EcoCode.Tests/Tests/DontCallFunctionsInLoopConditionsUnitTests.cs +++ b/src/EcoCode.Tests/Tests/EC69_DontCallFunctionsInLoopConditionsUnitTests.cs @@ -4,7 +4,7 @@ public class DontCallFunctionsInLoopConditionsUnitTests { private static readonly VerifyDlg VerifyAsync = CodeFixVerifier.VerifyAsync< - DontCallFunctionsInLoopConditions, + DontCallFunctionsInLoopConditionsAnalyzer, DontCallFunctionsInLoopConditionsCodeFixProvider>; [TestMethod] diff --git a/src/EcoCode.Tests/Tests/DontExecuteSqlCommandsInLoopsUnitTests.cs b/src/EcoCode.Tests/Tests/EC72_DontExecuteSqlCommandsInLoopsUnitTests.cs similarity index 96% rename from src/EcoCode.Tests/Tests/DontExecuteSqlCommandsInLoopsUnitTests.cs rename to src/EcoCode.Tests/Tests/EC72_DontExecuteSqlCommandsInLoopsUnitTests.cs index 3fa279d2..b19f1005 100644 --- a/src/EcoCode.Tests/Tests/DontExecuteSqlCommandsInLoopsUnitTests.cs +++ b/src/EcoCode.Tests/Tests/EC72_DontExecuteSqlCommandsInLoopsUnitTests.cs @@ -4,7 +4,7 @@ public class DontExecuteSqlCommandsInLoopsUnitTests { private static readonly VerifyDlg VerifyAsync = CodeFixVerifier.VerifyAsync< - DontExecuteSqlCommandsInLoops, + DontExecuteSqlCommandsInLoopsAnalyzer, DontExecuteSqlCommandsInLoopsCodeFixProvider>; [TestMethod] diff --git a/src/EcoCode.Tests/Tests/DontConcatenateStringsInLoopsUnitTests.cs b/src/EcoCode.Tests/Tests/EC75_DontConcatenateStringsInLoopsUnitTests.cs similarity index 97% rename from src/EcoCode.Tests/Tests/DontConcatenateStringsInLoopsUnitTests.cs rename to src/EcoCode.Tests/Tests/EC75_DontConcatenateStringsInLoopsUnitTests.cs index be346eeb..4757fcba 100644 --- a/src/EcoCode.Tests/Tests/DontConcatenateStringsInLoopsUnitTests.cs +++ b/src/EcoCode.Tests/Tests/EC75_DontConcatenateStringsInLoopsUnitTests.cs @@ -4,7 +4,7 @@ public class DontConcatenateStringsInLoopsUnitTests { private static readonly VerifyDlg VerifyAsync = CodeFixVerifier.VerifyAsync< - DontConcatenateStringsInLoops, + DontConcatenateStringsInLoopsAnalyzer, DontConcatenateStringsInLoopsCodeFixProvider>; [TestMethod] diff --git a/src/EcoCode.Tests/Tests/EC81_SpecifyStructLayoutUnitTests.cs b/src/EcoCode.Tests/Tests/EC81_SpecifyStructLayoutUnitTests.cs new file mode 100644 index 00000000..2a427694 --- /dev/null +++ b/src/EcoCode.Tests/Tests/EC81_SpecifyStructLayoutUnitTests.cs @@ -0,0 +1,92 @@ +namespace EcoCode.Tests; + +[TestClass] +public class SpecifyStructLayoutUnitTests +{ + private static readonly VerifyDlg VerifyAsync = CodeFixVerifier.VerifyAsync< + SpecifyStructLayoutAnalyzer, + SpecifyStructLayoutCodeFixProvider>; + + [TestMethod] + public async Task EmptyCodeAsync() => await VerifyAsync("").ConfigureAwait(false); + + [TestMethod] + public async Task EmptyStructWithNoLayoutAsync() => await VerifyAsync( + "public record struct TestStruct;").ConfigureAwait(false); + + [TestMethod] + public async Task ValuePropWithNoLayoutAsync() => await VerifyAsync( + "public record struct TestStruct(int A);").ConfigureAwait(false); + + [TestMethod] + public async Task ReferencePropWithNoLayoutAsync() => await VerifyAsync( + "public record struct TestStruct(string A);").ConfigureAwait(false); + + [TestMethod] + // For some reason this test doesn't pass with the 'record' syntax on GitHub, but it passes locally.. + public async Task ValuePropsWithNoLayoutAsync() => await VerifyAsync(""" + public struct [|TestStruct|] + { + public int A { get; set; } + public double B { get; set; } + }; + """, + fixedSource: """ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Auto)] + public struct TestStruct + { + public int A { get; set; } + public double B { get; set; } + }; + """).ConfigureAwait(false); + + [TestMethod] + public async Task ValuePropsWithLayoutAsync() => await VerifyAsync(""" + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Auto)] + public record struct TestStruct(int A, double B); + """).ConfigureAwait(false); + + [TestMethod] + public async Task ValueAndReferencePropsWithNoLayoutAsync() => await VerifyAsync( + "public record struct TestStruct(int A, string B);").ConfigureAwait(false); + + [TestMethod] + // For some reason this test doesn't pass with the 'record' syntax on GitHub, but it passes locally.. + public async Task AdditionalValuePropsWithNoLayout1Async() => await VerifyAsync(""" + public struct [|TestStruct|] + { + public int A { get; set; } + public double B { get; set; } + public int C { get; set; } + }; + """, + fixedSource: """ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Auto)] + public struct TestStruct + { + public int A { get; set; } + public double B { get; set; } + public int C { get; set; } + }; + """).ConfigureAwait(false); + + [TestMethod] + public async Task AdditionalValueFieldsWithNoLayout2Async() => await VerifyAsync(""" + using System; + + public record struct [|TestStruct|](bool A, int B, char C, short D, ulong E, DateTime F); + """, + fixedSource: """ + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Auto)] + public record struct [|TestStruct|](bool A, int B, char C, short D, ulong E, DateTime F); + """).ConfigureAwait(false); +} diff --git a/src/EcoCode.Vsix/EcoCode.Vsix.csproj b/src/EcoCode.Vsix/EcoCode.Vsix.csproj index 5d1c5dfb..5743ed22 100644 --- a/src/EcoCode.Vsix/EcoCode.Vsix.csproj +++ b/src/EcoCode.Vsix/EcoCode.Vsix.csproj @@ -38,10 +38,12 @@ + + - +