Skip to content

Commit

Permalink
[EC81] Specify struct layout (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
Djoums authored Apr 2, 2024
1 parent 37a1f3c commit 8af586f
Show file tree
Hide file tree
Showing 33 changed files with 412 additions and 94 deletions.
9 changes: 8 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -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]

Expand All @@ -13,7 +16,6 @@ tab_width = 4

# New line preferences
end_of_line = crlf
insert_final_newline = true

#### .NET Coding Conventions ####

Expand Down Expand Up @@ -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
40 changes: 20 additions & 20 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.1.1" />
<PackageVersion Include="DotNet.ReproducibleBuilds.Isolated" Version="1.1.1" />
<PackageVersion Include="EcoCode" Version="1.0.1-beta7" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.MSTest" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.MSTest" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.VSSDK.BuildTools" Version="17.9.3174" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageVersion Include="MSTest.TestFramework" Version="3.2.2" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.1.1" />
<PackageVersion Include="DotNet.ReproducibleBuilds.Isolated" Version="1.1.1" />
<PackageVersion Include="EcoCode" Version="1.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.MSTest" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.MSTest" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.VSSDK.BuildTools" Version="17.9.3174" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageVersion Include="MSTest.TestFramework" Version="3.2.2" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion EcoCode.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------
Expand Down
2 changes: 1 addition & 1 deletion SharedAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
using System.Runtime.InteropServices;

[assembly: ComVisible(false)]
[assembly: CLSCompliant(false)]
[assembly: CLSCompliant(false)]
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/// <summary>Analyzer for don't call loop invariant functions in loop conditions.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DontCallFunctionsInLoopConditions : DiagnosticAnalyzer
public sealed class DontCallFunctionsInLoopConditionsAnalyzer : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SyntaxKind> SyntaxKinds = [
SyntaxKind.ForStatement,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -98,4 +102,4 @@ static void RemoveMutatedSymbols(IEnumerable<SyntaxNode> nodes, HashSet<ISymbol>
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/// <summary>Analyzer for don't execute SQL commands in loops.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DontExecuteSqlCommandsInLoops : DiagnosticAnalyzer
public sealed class DontExecuteSqlCommandsInLoopsAnalyzer : DiagnosticAnalyzer
{
/// <summary>The diagnostic descriptor.</summary>
public static DiagnosticDescriptor Descriptor { get; } = new(
Expand Down Expand Up @@ -40,4 +40,4 @@ symbol.Name is "ExecuteNonQuery" or "ExecuteScalar" or "ExecuteReader" &&
}, SyntaxKind.InvocationExpression);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/// <summary>Analyzer for don't concatenate strings in loops.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DontConcatenateStringsInLoops : DiagnosticAnalyzer
public sealed class DontConcatenateStringsInLoopsAnalyzer : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SyntaxKind> SyntaxKinds = [
SyntaxKind.ForStatement,
Expand Down Expand Up @@ -48,4 +48,4 @@ assignment.Left is IdentifierNameSyntax identifierName &&
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace EcoCode.Analyzers;

/// <summary>Analyzer for specify struct layout.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class SpecifyStructLayoutAnalyzer : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SymbolKind> SymbolKinds = [SymbolKind.NamedType];

/// <summary>The diagnostic descriptor.</summary>
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));

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Descriptor];

/// <inheritdoc/>
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));
}
}
13 changes: 3 additions & 10 deletions src/EcoCode.Analyzers/EcoCode.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DotNet.ReproducibleBuilds">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="DotNet.ReproducibleBuilds.Isolated">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="DotNet.ReproducibleBuilds.Isolated" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" IncludeAssets="compile" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/EcoCode.Analyzers/Rule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
public sealed class DontCallFunctionsInLoopConditionsCodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [DontCallFunctionsInLoopConditions.Descriptor.Id];
public override ImmutableArray<string> FixableDiagnosticIds => [DontCallFunctionsInLoopConditionsAnalyzer.Descriptor.Id];

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
public sealed class DontExecuteSqlCommandsInLoopsCodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [DontExecuteSqlCommandsInLoops.Descriptor.Id];
public override ImmutableArray<string> FixableDiagnosticIds => [DontExecuteSqlCommandsInLoopsAnalyzer.Descriptor.Id];

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
public sealed class DontConcatenateStringsInLoopsCodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [DontConcatenateStringsInLoops.Descriptor.Id];
public override ImmutableArray<string> FixableDiagnosticIds => [DontConcatenateStringsInLoopsAnalyzer.Descriptor.Id];

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>The code fix provider for use struct layout.</summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SpecifyStructLayoutCodeFixProvider)), Shared]
public sealed class SpecifyStructLayoutCodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [SpecifyStructLayoutAnalyzer.Descriptor.Id];

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
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<Document> 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();
}
}
14 changes: 4 additions & 10 deletions src/EcoCode.CodeFixes/EcoCode.CodeFixes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,14 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DotNet.ReproducibleBuilds">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="DotNet.ReproducibleBuilds.Isolated">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="DotNet.ReproducibleBuilds.Isolated" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" IncludeAssets="compile" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EcoCode.Analyzers\EcoCode.Analyzers.csproj" />
<ProjectReference Include="..\EcoCode.Shared\EcoCode.Shared.csproj" />
</ItemGroup>

</Project>
</Project>
3 changes: 2 additions & 1 deletion src/EcoCode.CodeFixes/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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;
global using System.Threading.Tasks;
Loading

0 comments on commit 8af586f

Please sign in to comment.