-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[EC82] Variable can be made constant (#21)
- Loading branch information
Showing
18 changed files
with
1,130 additions
and
711 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
src/EcoCode.Analyzers/Analyzers/EC82_VariableCanBeMadeConstantAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
src/EcoCode.CodeFixes/CodeFixes/EC82_VariableCanBeMadeConstantCodeFixProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
src/EcoCode.Tests/LiveWarnings/EC82_VariableCanBeMadeConstantLiveWarnings.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.