Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/CSharp/CodeCracker/CodeCracker.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
<Compile Include="Design\InconsistentAccessibility\InconsistentAccessibilityInMethodParameter.cs" />
<Compile Include="Design\InconsistentAccessibility\InconsistentAccessibilityInfoProvider.cs" />
<Compile Include="Design\InconsistentAccessibility\InconsistentAccessibilityInMethodReturnType.cs" />
<Compile Include="Design\MakeMethodStaticAnalyzer.cs" />
<Compile Include="Design\MakeMethodStaticCodeFixProvider.cs" />
<Compile Include="Extensions\CSharpAnalyzerExtensions.cs" />
<Compile Include="Performance\UseStaticRegexIsMatchAnalyzer.cs" />
<Compile Include="Performance\StringBuilderInLoopCodeFixProvider.cs" />
Expand Down Expand Up @@ -271,4 +273,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
63 changes: 63 additions & 0 deletions src/CSharp/CodeCracker/Design/MakeMethodStaticAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Linq;
using System.Collections.Immutable;

namespace CodeCracker.CSharp.Design
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MakeMethodStaticAnalyzer : DiagnosticAnalyzer
{
internal const string Title = "Use static method";
internal const string MessageFormat = "Make '{0}' method static.";
internal const string Category = SupportedCategories.Design;
const string Description = "If the method is not referencing any instance variable and if you are " +
"not creating a virtual, abstract, new or partial method, and if it is not a method override, " +
"your instance method may be changed to a static method.";

internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId.MakeMethodStatic.ToDiagnosticId(),
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
true,
description: Description,
helpLinkUri: HelpLink.ForDiagnostic(DiagnosticId.MakeMethodStatic));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context) =>
context.RegisterSyntaxNodeAction(LanguageVersion.CSharp6, AnalyzeMethod, SyntaxKind.MethodDeclaration);

private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
{
if (context.IsGenerated()) return;
var method = (MethodDeclarationSyntax)context.Node;
if (method.Modifiers.Any(
SyntaxKind.StaticKeyword,
SyntaxKind.PartialKeyword,
SyntaxKind.VirtualKeyword,
SyntaxKind.NewKeyword,
SyntaxKind.AbstractKeyword,
SyntaxKind.OverrideKeyword)) return;
if (method.Body == null)
{
if (method.ExpressionBody?.Expression == null) return;
var dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(method.ExpressionBody.Expression);
if (!dataFlowAnalysis.Succeeded) return;
if (dataFlowAnalysis.DataFlowsIn.Any(inSymbol => inSymbol.Name == "this")) return;
}
else if (method.Body.Statements.Count > 0)
{
var dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(method.Body);
if (!dataFlowAnalysis.Succeeded) return;
if (dataFlowAnalysis.DataFlowsIn.Any(inSymbol => inSymbol.Name == "this")) return;
}
var diagnostic = Diagnostic.Create(Rule, method.Identifier.GetLocation(), method.Identifier.ValueText);
context.ReportDiagnostic(diagnostic);
}
}
}
97 changes: 97 additions & 0 deletions src/CSharp/CodeCracker/Design/MakeMethodStaticCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeCracker.CSharp.Design
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MakeMethodStaticCodeFixProvider)), Shared]
public class MakeMethodStaticCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticId.MakeMethodStatic.ToDiagnosticId());

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

public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
context.RegisterCodeFix(CodeAction.Create("Make static", c => MakeNameOfAsync(context.Document, diagnostic, c)), diagnostic);
return Task.FromResult(0);
}

private static readonly SyntaxToken staticToken = SyntaxFactory.Token(SyntaxKind.StaticKeyword);

private static async Task<Solution> MakeNameOfAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var diagnosticSpan = diagnostic.Location.SourceSpan;
var method = (MethodDeclarationSyntax)root.FindNode(diagnosticSpan);
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var methodSymbol = semanticModel.GetDeclaredSymbol(method);
var methodClassName = methodSymbol.ContainingType.Name;
var references = await SymbolFinder.FindReferencesAsync(methodSymbol, document.Project.Solution, cancellationToken).ConfigureAwait(false);
var documentGroups = references.SelectMany(r => r.Locations).GroupBy(loc => loc.Document);
var newSolution = UpdateMainDocument(document, root, method, documentGroups);
newSolution = await UpdateReferencingDocuments(document, methodClassName, documentGroups, newSolution, cancellationToken);
return newSolution;
}

private static Solution UpdateMainDocument(Document document, SyntaxNode root, MethodDeclarationSyntax method, IEnumerable<IGrouping<Document, ReferenceLocation>> documentGroups)
{
var mainDocGroup = documentGroups.FirstOrDefault(dg => dg.Key.Equals(document));
SyntaxNode newRoot;
if (mainDocGroup == null)
{
newRoot = root.ReplaceNode(method, method.AddModifiers(staticToken));
}
else
{
var diagnosticNodes = mainDocGroup.Select(referenceLocation => root.FindNode(referenceLocation.Location.SourceSpan)).ToList();
newRoot = root.TrackNodes(diagnosticNodes.Union(new[] { method }));
newRoot = newRoot.ReplaceNode(newRoot.GetCurrentNode(method), method.AddModifiers(staticToken));
foreach (var diagnosticNode in diagnosticNodes)
{
var invocationExpression = newRoot.GetCurrentNode(diagnosticNode).FirstAncestorOrSelfOfType<InvocationExpressionSyntax>().Expression;
if (invocationExpression.IsKind(SyntaxKind.IdentifierName)) continue;
var memberAccess = (MemberAccessExpressionSyntax)invocationExpression;
var newMemberAccessParent = memberAccess.Parent.ReplaceNode(memberAccess, memberAccess.Name)
.WithAdditionalAnnotations(Formatter.Annotation);
newRoot = newRoot.ReplaceNode(memberAccess.Parent, newMemberAccessParent);
}
}
var newSolution = document.Project.Solution.WithDocumentSyntaxRoot(document.Id, newRoot);
return newSolution;
}

private static async Task<Solution> UpdateReferencingDocuments(Document document, string methodClassName, IEnumerable<IGrouping<Document, ReferenceLocation>> documentGroups, Solution newSolution, CancellationToken cancellationToken)
{
var methodIdentifier = SyntaxFactory.IdentifierName(methodClassName);
foreach (var documentGroup in documentGroups)
{
var referencingDocument = documentGroup.Key;
if (referencingDocument.Equals(document)) continue;
var newReferencingRoot = await referencingDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var diagnosticNodes = documentGroup.Select(referenceLocation => newReferencingRoot.FindNode(referenceLocation.Location.SourceSpan)).ToList();
newReferencingRoot = newReferencingRoot.TrackNodes(diagnosticNodes);
foreach (var diagnosticNode in diagnosticNodes)
{
var memberAccess = (MemberAccessExpressionSyntax)newReferencingRoot.GetCurrentNode(diagnosticNode).FirstAncestorOrSelfOfType<InvocationExpressionSyntax>().Expression;
var newMemberAccess = memberAccess.ReplaceNode(memberAccess.Expression, methodIdentifier)
.WithAdditionalAnnotations(Formatter.Annotation);
newReferencingRoot = newReferencingRoot.ReplaceNode(memberAccess, newMemberAccess);
}
newSolution = newSolution.WithDocumentSyntaxRoot(referencingDocument.Id, newReferencingRoot);
}
return newSolution;
}
}
}
11 changes: 11 additions & 0 deletions src/CSharp/CodeCracker/Extensions/CSharpAnalyzerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,16 @@ public static bool IsKind(this SyntaxNodeOrToken nodeOrToken, params SyntaxKind[
}

public static bool IsNotKind(this SyntaxNode node, params SyntaxKind[] kinds) => !node.IsKind(kinds);

public static bool Any(this SyntaxTokenList list, SyntaxKind kind1, SyntaxKind kind2) =>
list.IndexOf(kind1) >= 0 || list.IndexOf(kind2) >= 0;

public static bool Any(this SyntaxTokenList list, SyntaxKind kind1, SyntaxKind kind2, params SyntaxKind[] kinds)
{
if (list.Any(kind1, kind2)) return true;
for (int i = 0; i < kinds.Length; i++)
if (list.IndexOf(kinds[i]) >= 0) return true;
return false;
}
}
}
1 change: 1 addition & 0 deletions src/Common/CodeCracker.Common/DiagnosticId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public enum DiagnosticId
UseStringEmpty = 84,
UseEmptyString = 88,
RemoveRedundantElseClause = 89,
MakeMethodStatic = 91,
ChangeAllToAny = 92,
}
}
3 changes: 2 additions & 1 deletion test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
<Compile Include="ChangeCultureTests.cs" />
<Compile Include="Design\InconsistentAccessibilityTests.MethodParameter.cs" />
<Compile Include="Design\InconsistentAccessibilityTests.MethodReturnType.cs" />
<Compile Include="Design\MakeMethodStaticTests.cs" />
<Compile Include="GeneratedCodeAnalysisExtensionsTests.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="Performance\UseStaticRegexIsMatchTests.cs" />
Expand Down Expand Up @@ -261,4 +262,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
Loading