// // 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 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 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(); // 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(); var misplacedMembers = new List(); 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 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() .First(); if(!RenodeClassMembersOrderAnalyzer.TryGetClassMemberOrder(misplacedDeclaration, out var misplacedOrder)) { return document; } var classMembers = misplacedDeclaration .Parent .ChildNodes() .OfType(); 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(); } } }