Skip to content

Commit

Permalink
[EC82] Variable can be made constant (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
Djoums authored Apr 3, 2024
1 parent 8af586f commit 8746bf7
Show file tree
Hide file tree
Showing 18 changed files with 1,130 additions and 711 deletions.
2 changes: 2 additions & 0 deletions EcoCode.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Packages.props = Directory.Packages.props
global.json = global.json
icon.jpeg = icon.jpeg
LICENCE.md = LICENCE.md
NOTICE.md = NOTICE.md
README.md = README.md
SharedAssemblyInfo.cs = SharedAssemblyInfo.cs
EndProjectSection
Expand Down
674 changes: 674 additions & 0 deletions LICENCE.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This project incorporates ideas and code from the following open-source projects, whose contribution to the open-source community is greatly appreciated:

**Roslynator**: https://github.com/dotnet/roslynator. Copyright (c) .NET Foundation and Contributors.
Licensed under the Apache License, Version 2.0; full text can be found at https://www.apache.org/licenses/LICENSE-2.0.

**Meziantou.Analyzer**: https://github.com/meziantou/Meziantou.Analyzer. Copyright (c) Gérald Barré.
Licensed under the MIT License; full text can be found at https://opensource.org/licenses/MIT.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Both the EcoCode NuGet package and Visual Studio extension target .Net Standard
|[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|⚠️|✔️|✔️|
|[EC82](https://github.com/green-code-initiative/ecoCode/blob/main/ecocode-rules-specifications/src/main/rules/EC82/csharp/EC82.asciidoc)|Variable can be made constant|ℹ️|✔️|✔️|

🤝 Contribution
---------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed class DontCallFunctionsInLoopConditionsAnalyzer : DiagnosticAnalyz
Rule.Categories.Performance,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: string.Empty,
description: null,
helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC69_DontCallFunctionsInLoopConditions));

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public sealed class DontExecuteSqlCommandsInLoopsAnalyzer : DiagnosticAnalyzer
Rule.Categories.Performance,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: string.Empty,
description: null,
helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC72_DontExecuteSqlCommandsInLoops));

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class DontConcatenateStringsInLoopsAnalyzer : DiagnosticAnalyzer
Rule.Categories.Performance,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: string.Empty,
description: null,
helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC75_DontConcatenateStringsInLoops));

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public sealed class SpecifyStructLayoutAnalyzer : DiagnosticAnalyzer
Rule.Categories.Performance,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: string.Empty,
description: null,
helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC81_UseStructLayout));

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace EcoCode.Analyzers;

/// <summary>Analyzer for variable can be made constant.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class VariableCanBeMadeConstantAnalyzer : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SyntaxKind> SyntaxKinds = [SyntaxKind.LocalDeclarationStatement];

/// <summary>The diagnostic descriptor.</summary>
public static DiagnosticDescriptor Descriptor { get; } = new(
Rule.Ids.EC82_VariableCanBeMadeConstant,
title: "Variable can be made constant",
messageFormat: "Variable can be made constant",
Rule.Categories.Usage,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC82_VariableCanBeMadeConstant));

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

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(static context => AnalyzeNode(context), SyntaxKinds);
}

private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

// Make sure the declaration isn't already const
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
return;

// Ensure that all variables in the local declaration have initializers that are assigned with constant values
var variableType = context.SemanticModel.GetTypeInfo(localDeclaration.Declaration.Type, context.CancellationToken).ConvertedType;
if (variableType is null) return;
foreach (var variable in localDeclaration.Declaration.Variables)
{
var initializer = variable.Initializer;
if (initializer is null) return;

var constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
if (!constantValue.HasValue) return;

// Ensure that the initializer value can be converted to the type of the local declaration without a user-defined conversion.
var conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined) return;

// Special cases:
// * If the constant value is a string, the type of the local declaration must be string
// * If the constant value is null, the type of the local declaration must be a reference type
if (constantValue.Value is string)
{
if (variableType.SpecialType is not SpecialType.System_String) return;
}
else if (variableType.IsReferenceType && constantValue.Value is not null)
{
return;
}
}

// Perform data flow analysis on the local declaration
var dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
if (dataFlowAnalysis is null) return;

foreach (var variable in localDeclaration.Declaration.Variables)
{
// Retrieve the local symbol for each variable in the local declaration and ensure that it is not written outside of the data flow analysis region
var variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (variableSymbol is null || dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
return;
}

context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Node.GetLocation()));
}
}
1 change: 1 addition & 0 deletions src/EcoCode.Analyzers/Rule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ public static class Ids
public const string EC72_DontExecuteSqlCommandsInLoops = "EC72";
public const string EC75_DontConcatenateStringsInLoops = "EC75";
public const string EC81_UseStructLayout = "EC81";
public const string EC82_VariableCanBeMadeConstant = "EC82";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
context.RegisterCodeFix(
CodeAction.Create(
"Add Auto StructLayout attribute",
ct => Refactor(context.Document, nodeToFix, LayoutKind.Auto, ct),
ct => RefactorAsync(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),
ct => RefactorAsync(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)
private static async Task<Document> RefactorAsync(Document document, SyntaxNode nodeToFix, LayoutKind layoutKind, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using System.Threading;

namespace EcoCode.CodeFixes;

/// <summary>The code fix provider for variable can be made constant.</summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(VariableCanBeMadeConstantCodeFixProvider)), Shared]
public sealed class VariableCanBeMadeConstantCodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [VariableCanBeMadeConstantAnalyzer.Descriptor.Id];

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

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (context.Diagnostics.Length == 0) return;

var document = context.Document;
var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null) return;

var diagnostic = context.Diagnostics[0];
var parent = root.FindToken(diagnostic.Location.SourceSpan.Start).Parent;
if (parent is null) return;

foreach (var node in parent.AncestorsAndSelf())
{
if (node is not LocalDeclarationStatementSyntax declaration) continue;
context.RegisterCodeFix(
CodeAction.Create(
title: "Make variable constant",
createChangedDocument: token => RefactorAsync(document, declaration, token),
equivalenceKey: "Make variable constant"),
diagnostic);
break;
}
}

private static async Task<Document> RefactorAsync(Document document, LocalDeclarationStatementSyntax localDecl, CancellationToken token)
{
// Remove the leading trivia from the local declaration.
var firstToken = localDecl.GetFirstToken();
var leadingTrivia = firstToken.LeadingTrivia;
var trimmedLocal = leadingTrivia.Any()
? localDecl.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty))
: localDecl;

// Create a const token with the leading trivia.
var constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

// If the type of the declaration is 'var', create a new type name for the inferred type.
var varDecl = localDecl.Declaration;
var varTypeName = varDecl.Type;
if (varTypeName.IsVar)
varDecl = await GetDeclarationForVarAsync(document, varDecl, varTypeName, token).ConfigureAwait(false);

// Produce the new local declaration with an annotation
var formattedLocal = trimmedLocal
.WithModifiers(trimmedLocal.Modifiers.Insert(0, constToken)) // Insert the const token into the modifiers list
.WithDeclaration(varDecl)
.WithAdditionalAnnotations(Formatter.Annotation);

// Replace the old local declaration with the new local declaration.
var oldRoot = await document.GetSyntaxRootAsync(token).ConfigureAwait(false);
return oldRoot is null ? document : document.WithSyntaxRoot(oldRoot.ReplaceNode(localDecl, formattedLocal));

static async Task<VariableDeclarationSyntax> GetDeclarationForVarAsync(Document document, VariableDeclarationSyntax varDecl, TypeSyntax varTypeName, CancellationToken token)
{
var semanticModel = await document.GetSemanticModelAsync(token).ConfigureAwait(false);

if (semanticModel is null || semanticModel.GetAliasInfo(varTypeName, token) is not null)
return varDecl; // Special case: Ensure that 'var' isn't actually an alias to another type (e.g. using var = System.String)

var type = semanticModel.GetTypeInfo(varTypeName, token).ConvertedType;
if (type is null || type.Name == "var") return varDecl; // Special case: Ensure that 'var' isn't actually a type named 'var'

// Create a new TypeSyntax for the inferred type. Be careful to keep any leading and trailing trivia from the var keyword.
return varDecl.WithType(SyntaxFactory
.ParseTypeName(type.ToDisplayString())
.WithLeadingTrivia(varTypeName.GetLeadingTrivia())
.WithTrailingTrivia(varTypeName.GetTrailingTrivia())
.WithAdditionalAnnotations(Simplifier.Annotation));
}
}
}
7 changes: 4 additions & 3 deletions src/EcoCode.Package/EcoCode.Package.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
</ItemGroup>

<ItemGroup>
<None Update="tools\*.ps1" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="" />
<None Include="..\..\icon.jpeg" Pack="true" PackagePath="" />
<None Include="..\..\README.md" Pack="true" PackagePath="" />
<None Update="tools\*.ps1" Pack="true" PackagePath="" CopyToOutputDirectory="PreserveNewest" />
<Content Include="..\..\icon.jpeg" Pack="true" PackagePath="" CopyToOutputDirectory="PreserveNewest" />
<Content Include="..\..\NOTICE.md" Pack="true" PackagePath="" CopyToOutputDirectory="PreserveNewest" />
<Content Include="..\..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace EcoCode.Tests;

internal static class VariableCanBeMadeConstantLiveWarnings
{
public static void Test1()
{
int i = 0; // EC82: Variable can be made constant
Console.WriteLine(i);

int j = 0;
Console.WriteLine(j++);

const int k = 0;
Console.WriteLine(k);

int m = DateTime.Now.DayOfYear;
Console.WriteLine(m);

int n = 0, o = 0; // EC82: Variable can be made constant
Console.WriteLine(n);
Console.WriteLine(o);

object p = "abc";
Console.WriteLine(p);

string q = "abc"; // EC82: Variable can be made constant
Console.WriteLine(q);

var r = 4; // EC82: Variable can be made constant
Console.WriteLine(r);

var s = "abc"; // EC82: Variable can be made constant
Console.WriteLine(s);
}
}
Loading

0 comments on commit 8746bf7

Please sign in to comment.