218 lines
9.2 KiB
C#
218 lines
9.2 KiB
C#
//
|
|
// Copyright (c) 2010-2025 Antmicro
|
|
//
|
|
// This file is licensed under the MIT License.
|
|
// Full license text is available in 'licenses/MIT.txt'.
|
|
//
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Composition;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CodeActions;
|
|
using Microsoft.CodeAnalysis.CodeFixes;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Editing;
|
|
|
|
namespace Antmicro.Renode.CustomAnalyzers
|
|
{
|
|
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RenodeClassMembersOrderFixProvider)), Shared]
|
|
public class RenodeClassMembersOrderFixProvider : CodeFixProvider
|
|
{
|
|
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
|
|
ImmutableArray.Create(RenodeClassMembersOrderAnalyzer.DiagnosticId);
|
|
|
|
public override FixAllProvider GetFixAllProvider() =>
|
|
FixAllProvider.Create(async (context, document, _) =>
|
|
await FixDocumentClassMembersOrder(document, context.CancellationToken).ConfigureAwait(false));
|
|
|
|
public override Task RegisterCodeFixesAsync(CodeFixContext context)
|
|
{
|
|
var document = context.Document;
|
|
// We have only one entry in FixableDiagnosticIds,
|
|
// so there should be just one entry on the list.
|
|
var diagnostic = context.Diagnostics.First();
|
|
|
|
var codeAction = CodeAction.Create(
|
|
title: "Fix Renode class member order",
|
|
createChangedDocument: c => FixSingleClassMemberOrder(document, diagnostic, c),
|
|
equivalenceKey: "RenodeClassMemberOrder"
|
|
);
|
|
|
|
context.RegisterCodeFix(codeAction, diagnostic);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task<Document> FixDocumentClassMembersOrder(
|
|
Document document,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
|
|
|
|
// This fix provider is capable of fixing members order also within nested calsses, on arbitrary level.
|
|
// In order to do so, it must handle nested classes first. Since we list members recursively
|
|
// in document prefix order, we just have to reverse list to start with nested classes.
|
|
var classes = editor
|
|
.OriginalRoot
|
|
.DescendantNodes()
|
|
.Where(node => node.IsKind(SyntaxKind.ClassDeclaration))
|
|
.Reverse();
|
|
|
|
foreach(var cls in classes)
|
|
{
|
|
var currentRoot = editor.GetChangedRoot();
|
|
|
|
var declarations = cls
|
|
.ChildNodes()
|
|
.OfType<MemberDeclarationSyntax>();
|
|
|
|
// We first find all misplaced members and remove them from tree.
|
|
// When we reinsert removed nodes to tree, they are treated as new nodes, so we cannot reference them
|
|
// in tree operations (i.e. we cannot use them as reference when inserting new nodes).
|
|
// We also remember all well ordered nodes to use them as references when we reinsert misplaced nodes.
|
|
var wellOrderedMembers = new List<MemberDeclarationSyntax>();
|
|
var misplacedMembers = new List<MemberDeclarationSyntax>();
|
|
|
|
var currentMember = RenodeClassMembersOrderAnalyzer.ClassMemberOrder.StaticConstructor;
|
|
foreach(var declaration in declarations)
|
|
{
|
|
if(RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(declaration, out var nextMember))
|
|
{
|
|
if(nextMember < currentMember)
|
|
{
|
|
misplacedMembers.Add(declaration);
|
|
editor.RemoveNode(declaration, SyntaxRemoveOptions.KeepNoTrivia);
|
|
}
|
|
else
|
|
{
|
|
wellOrderedMembers.Add(declaration);
|
|
currentMember = nextMember;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This class already satisfies Renode class members order rule
|
|
if(misplacedMembers.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// We know that this class content will be modified, so we want to track this class
|
|
// in case we need to move it later
|
|
editor.TrackNode(cls);
|
|
|
|
// At this point all members stored in wellOrderedMembers and misplacedMembers
|
|
// have corresponding ClassMemberOrder, so we no longer need to check every call
|
|
// to RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder.
|
|
misplacedMembers.Sort((lhs, rhs) =>
|
|
{
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(lhs, out var lhsOrder);
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(rhs, out var rhsOrder);
|
|
return lhsOrder.CompareTo(rhsOrder);
|
|
});
|
|
|
|
// If we are here, we know that there is at least one correct memeber and at least one misplaced member.
|
|
// So calling MoveNext on Enumerator is safe.
|
|
var correctMemberEnumerator = wellOrderedMembers.GetEnumerator();
|
|
correctMemberEnumerator.MoveNext();
|
|
|
|
var correctMember = correctMemberEnumerator.Current;
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(correctMember, out var correctOrder);
|
|
|
|
// We keep last valid order to easily break loop
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(wellOrderedMembers.Last(), out var lastCorrectOrder);
|
|
|
|
foreach(var misplacedMember in misplacedMembers)
|
|
{
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(misplacedMember, out var misplacedOrder);
|
|
if(misplacedOrder > lastCorrectOrder)
|
|
{
|
|
break;
|
|
}
|
|
|
|
while(misplacedOrder > correctOrder)
|
|
{
|
|
correctMemberEnumerator.MoveNext();
|
|
correctMember = correctMemberEnumerator.Current;
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(correctMember, out correctOrder);
|
|
}
|
|
|
|
var upToDateMember = currentRoot.GetCurrentNode(misplacedMember) ?? misplacedMember;
|
|
editor.InsertBefore(correctMember, upToDateMember);
|
|
}
|
|
|
|
// correctMember now points to the last valid member.
|
|
// We have to perform InsertAfter on remaining misplaced nodes
|
|
// in reverse order to keep valid members order.
|
|
misplacedMembers.Reverse();
|
|
foreach(var misplacedMember in misplacedMembers)
|
|
{
|
|
RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(misplacedMember, out var misplacedOrder);
|
|
if(misplacedOrder <= lastCorrectOrder)
|
|
{
|
|
break;
|
|
}
|
|
var upToDateMember = currentRoot.GetCurrentNode(misplacedMember) ?? misplacedMember;
|
|
editor.InsertAfter(correctMember, upToDateMember);
|
|
}
|
|
}
|
|
|
|
return editor.GetChangedDocument();
|
|
}
|
|
|
|
private async Task<Document> FixSingleClassMemberOrder(
|
|
Document document,
|
|
Diagnostic diagnostic,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
|
|
var root = editor.OriginalRoot;
|
|
|
|
var misplacedDeclaration = root
|
|
.FindToken(diagnostic.Location.SourceSpan.Start)
|
|
.Parent
|
|
.AncestorsAndSelf()
|
|
.OfType<MemberDeclarationSyntax>()
|
|
.First();
|
|
|
|
if(!RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(misplacedDeclaration, out var misplacedOrder))
|
|
{
|
|
return document;
|
|
}
|
|
|
|
var classMembers = misplacedDeclaration
|
|
.Parent
|
|
.ChildNodes()
|
|
.OfType<MemberDeclarationSyntax>();
|
|
|
|
editor.RemoveNode(misplacedDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
|
|
|
|
var successor = classMembers
|
|
.FirstOrDefault(
|
|
node => RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(node, out var correctOrder) && misplacedOrder <= correctOrder);
|
|
|
|
// If there isn't any member, that is greater or equal than misplaced declaration,
|
|
// then it must be last declaration.
|
|
if(successor == null)
|
|
{
|
|
editor.InsertAfter(classMembers.Last(), misplacedDeclaration);
|
|
}
|
|
else
|
|
{
|
|
editor.InsertBefore(successor, misplacedDeclaration);
|
|
}
|
|
|
|
return editor.GetChangedDocument();
|
|
}
|
|
}
|
|
}
|