Roslynによるリファクタリング

通常、リファクタリングはバグに苦労するようです。 過去のエラーを手動で単調に修正。 しかし、アクションをAを介した変換アルゴリズムに還元してBを取得できる場合、このプロセスを自動化してみませんか?







このようなケースは多くあります-依存関係の反転(アーキテクチャの変更の例として)、属性の追加、アスペクト(エンドツーエンド機能の追加の例)およびクラスとメソッドでのさまざまなコードレイアウトの導入、およびこの記事ではこのケースを検討します詳細に。







すべてのプロジェクトはAPIを使用します。 APIモジュール、コンポーネント、フレームワーク、オペレーティングシステム、パブリックサービスAPI-ほとんどの場合、これらのAPIはインターフェイスとして提供されます。 しかし、周囲のすべてが変化しているため、APIも変化しています。 新しいバージョンが表示され、廃止されたメソッドが表示されます。 プロジェクトをリファクタリングするオーバーヘッドなしで、自動的に新しいバージョンに切り替えることができればうれしいです。







ツール選択



Veeamは、開発者自身が必要と考えるすべてのツールを開発者に提供します。 そして、リファクタリングに役立つ最高のもの、ReSharperがあります。 しかし...







2015年、ReSharperに問題が発生しました 。 2016年初頭、問題RSRP-451569のステータスがSubmittedに変更されました。 また、2016年には、リクエストが更新されました







最新の更新を確認しました。必要な機能はありません。リゾルバーからのプロンプトもJetBrains.Annotationsの特別な属性もありません。 ReSharperがこの機能を持つのを待つ代わりに、私はこのタスクを自分で行うことにしました。







.NETコードリファクタリングで作業するときに最初に頭に浮かぶのは、IntelliSenseです。 しかし、私の意見では、そのAPIはかなり複雑でわかりにくいものであり、Visual Studioとも強く結びついています。 次に、DTE(EnvDTE)-開発ツール環境のようなものに出会いました。実際、それはUIまたはコマンドラインからアクセスできるスタジオのすべての機能へのインターフェイスであり、Visual Studioで実行できる一連のアクションを自動化できます。 しかし、DTEは不快なものであり、常にアクションコンテキストが必要です。 プログラマー全体をエミュレートします。 メソッドの定義の検索などの簡単なアクションは困難でした。 DTEでの作業の困難を克服する過程で、Alexander Kugushevによるビデオレポートに出会いました。









レポートは私に興味を持ち、Roslynの助けを借りてこのような問題を解決する方がはるかに自然であることに気づきました。 また、言語レベルでのコードの動作を理解するのに役立つ便利なSyntax Visualizerを忘れないでください。







そこで、ツールとしてRoslyn .NET Compiler Platformを選択しました。







タスクを自動化する



この問題を人為的な例で検討してください。 廃止されたメソッドを持つAPIがあるとし、それらを[Obsolete]



属性でマークし、どのメソッドを置き換える必要があるかをメッセージで示します







 public interface IAppService { [Obsolete("Use DoSomethingNew")] void DoSomething(); void DoSomethingNew(); [Obsolete("Use TestNew")] void Test(); void TestNew(); }
      
      





およびその実装は属性によって割り当てられていません







 public class DoService : IAppService { public void DoSomething() { Console.WriteLine("This is obsolete method. Use DoSomethingNew."); } public void DoSomethingNew() { Console.WriteLine("Good."); } public void Test() { Console.WriteLine("This is obsolete method. Use TestNew."); } public void TestNew() { Console.WriteLine("Good."); } }
      
      





インターフェイスの使用例。ここではIAppServiceを使用しているため、メソッドが非推奨であるというコンパイラ警告が表示されます。







  public class Test { public IAppService service { get; set; } public Test() { service = new DoService(); service.DoSomething(); service.Test(); } }
      
      





ここでは、このインターフェイスの実装のインスタンスを既に使用しているため、警告は表示されません。







 class Program { static void Main(string[] args) { //no warning highlighted var doService = new DoService(); doService.DoSomething(); //will be highlighted //IAppService service = doService as IAppService; //service.DoSomething(); doService.Test(); } static void Test() { var doService = new DoService(); doService.DoSomething(); doService.Test(); } }
      
      





状況を修正する







Roslynの使用には、宣言型プログラミングと関数型アプローチの使用が必要です。ここでの主なことは、メインゴールを設定し、それを慎重に記述することです。 私たちの主な目標は、古いメソッドをすべて新しいメソッドに置き換えることです。 記述します。







  1. 従来の方法を新しいものに置き換えます。

    どうやって?
  2. 廃止された新しいメソッドのペアを見つけて、廃止されたメソッドを新しいメソッドに置き換えます。

    どこ?
  3. APIクラスで、従来の新しいメソッドのペアを見つけて、従来のメソッドを新しいものに置き換えます。

    どうやって?
  4. APIクラスで[Obsolete]



    属性を持つメソッドの定義を見つけ、legacy-newメソッドのペアを見つけて、legacyメソッドを新しいものに置き換えます。

    新しいものはどこで入手できますか?
  5. [Obsolete]



    属性のメッセージには、見つかったメソッド定義の新しいメソッドの名前があり、APIクラスに[Obsolete]



    属性があります。ここで、古いメソッドと新しいメソッドのペアを見つけ、古いメソッドを新しいメソッドに置き換えます。

    交換する場所
  6. 廃止されたメソッドへのすべての参照について(一部のプロジェクトおよびクラスの例外を作成できますが)、 [Obsolete]



    属性のメッセージで、廃止されたペアを見つけるAPIクラスの[Obsolete]



    属性を持つ、見つかったメソッド定義の新しいメソッドの名前を見つけますは新しいメソッドであり、廃止されたメソッドを新しいメソッドに置き換えます。


リファクタリングアルゴリズムは、.NETコードの3つの柱であるドキュメント、構文ツリー、セマンティックモデルの操作の技術的側面の欠如を除いて準備ができています。 それはすべてラムダによく似ています。 それらで、私たちの主な目標を表現します。







インフラ

ソリューションのインフラストラクチャは、ドキュメント、構文ツリー、セマンティックモデルです。







 public Project Project { get; set; } public MSBuildWorkspace Workspace { get; set; } public Solution Solution { get; set; } public Refactorer(string solutionPath, string projectName) { // start Roslyn workspace Workspace = MSBuildWorkspace.Create(); // open solution we want to analyze Solution = Workspace.OpenSolutionAsync(solutionPath).Result; // find target project Project = Solution.Projects.FirstOrDefault(p => p.Name == projectName); } public void ReplaceObsoleteApiCalls(string interfaceClassName, string obsoleteMessagePattern) {...}
      
      





不足しているデータを外部から取得するには、ソリューションへの完全なパスが必要になります。APIを含むプロジェクトとそれが使用されるプロジェクトがあります-少し改良して、それらを異なるソリューションに配置できます。 また、APIが抽象クラスなどで構築される場合は、APIのクラス名も指定する必要があります。次に、Syntax Visualizerを使用して、このクラスの定義のタイプを調べます。







 var solutionPath = "..\Sample.sln"; var projectName = "ObsoleteApi"; var interfaceClassName = "IAppService"; var refactorererApi = new Refactorer(solutionPath, projectName); refactorererApi.ReplaceObsoleteApiCalls(interfaceClassName, "Use ");
      
      





ReplaceObsoleteApiCalls



プライベートインフラストラクチャ要素をReplaceObsoleteApiCalls









 var document = GetDocument(interfaceClassName); var model = document.GetSemanticModelAsync().Result; SyntaxNode root = document.GetSyntaxRootAsync().Result;
      
      





アルゴリズムに戻り、簡単な質問に答えます。最後から始める必要があります。

4. [Obsolete]



属性を持つメソッド定義を見つけます3. APIクラスで







 // direction from point 3 var targetInterfaceClass = root.DescendantNodes().OfType<InterfaceDeclarationSyntax>() .FirstOrDefault(c => c.Identifier.Text == interfaceClassName); var methodDeclarations = targetInterfaceClass.DescendantNodes().OfType<MethodDeclarationSyntax>().ToList(); var obsoleteMethods = methodDeclarations .Where(m => m.AttributeLists .FirstOrDefault(a => a.Attributes .FirstOrDefault(atr => (atr.Name as IdentifierNameSyntax).Identifier.Text == "Obsolete") != null) != null).ToList();
      
      





2.廃止された新しいメソッドのペアを見つける







 List<ObsoleteReplacement> replacementMap = new List<ObsoleteReplacement>(); foreach (var method in obsoleteMethods) { // find new mthod for replace - explain in point 5 var methodName = GetMethodName(obsoleteMessagePattern, method); if (methodDeclarations.FirstOrDefault(m => m.Identifier.Text == methodName) != null) { // find all reference of obsolete call - explain in point 6 var usingReferences = GetUsingReferences(model, method); replacementMap.Add(new ObsoleteReplacement() { ObsoleteMethod = SyntaxFactory.IdentifierName(method.Identifier.Text), ObsoleteReferences = usingReferences, NewMethod = SyntaxFactory.IdentifierName(methodName) }); } }
      
      





1.廃止されたメソッドを新しいメソッドに置き換えます







 private void UpdateSolutionWithAction(List<ObsoleteReplacement> replacementMap, Action<DocumentEditor, ObsoleteReplacement, SyntaxNode> action) { var workspace = MSBuildWorkspace.Create(); foreach (var item in replacementMap) { var solution = workspace.OpenSolutionAsync(Solution.FilePath).Result; var project = solution.Projects.FirstOrDefault(p => p.Name == Project.Name); foreach (var reference in item.ObsoleteReferences) { var docs = reference.Locations.Select(l => l.Document); foreach (var doc in docs) { var document = project.Documents.FirstOrDefault(d => d.Name == doc.Name); var documentEditor = DocumentEditor.CreateAsync(document).Result; action(documentEditor, item, document.GetSyntaxRootAsync().Result); document = documentEditor.GetChangedDocument(); solution = solution.WithDocumentSyntaxRoot(document.Id, document.GetSyntaxRootAsync().Result.NormalizeWhitespace()); } } var result = workspace.TryApplyChanges(solution); workspace.CloseSolution(); } UpdateRefactorerEnv(); } private void ReplaceMethod(DocumentEditor documentEditor, ObsoleteReplacement item, SyntaxNode root) { var identifiers = root.DescendantNodes().OfType<IdentifierNameSyntax>(); var usingTokens = identifiers.Where(i => i.Identifier.Text == item.ObsoleteMethod.Identifier.Text); foreach (var oldMethod in usingTokens) { // The Most Impotant Moment Of Point 1 documentEditor.ReplaceNode(oldMethod, item.NewMethod); } }
      
      





補助的な質問に答えます。

5. [Obsolete]



属性メッセージに、新しいメソッドの名前が表示されます







 private string GetMethodName(string obsoleteMessagePattern, MethodDeclarationSyntax method) { var message = GetAttributeMessage(method); int index = message.LastIndexOf(obsoleteMessagePattern) + obsoleteMessagePattern.Length; return message.Substring(index); } private static string GetAttributeMessage(MethodDeclarationSyntax method) { var obsoleteAttribute = method.AttributeLists.FirstOrDefault().Attributes.FirstOrDefault(atr => (atr.Name as IdentifierNameSyntax).Identifier.Text == "Obsolete"); var messageArgument = obsoleteAttribute.ArgumentList.DescendantNodes().OfType<AttributeArgumentSyntax>() .FirstOrDefault(arg => arg.ChildNodes().OfType<LiteralExpressionSyntax>().Count() != 0); var message = messageArgument.ChildNodes().FirstOrDefault().GetText(); return message.ToString().Trim('\"'); }
      
      





6.廃止されたメソッドへのすべての参照について(ただし、一部のプロジェクトおよびクラスでは例外を作成できます)







 private IEnumerable<ReferencedSymbol> GetUsingReferences(SemanticModel model, MethodDeclarationSyntax method) { var methodSymbol = model.GetDeclaredSymbol(method); var usingReferences = SymbolFinder.FindReferencesAsync(methodSymbol, Solution).Result.Where(r => r.Locations.Count() > 0); return usingReferences; }
      
      





例外の明確化は、次のフィルターで表すことができます。







 /// <param name="excludedClasses">Exclude method declarations that using in excluded classes in current Solution</param> private bool ContainInClasses(IEnumerable<ReferencedSymbol> usingReferences, List<string> excludedClasses) { if (excludedClasses.Count <= 0) { return false; } foreach (var reference in usingReferences) { foreach (var location in reference.Locations) { var node = location.Location.SourceTree.GetRoot().FindNode(location.Location.SourceSpan); ClassDeclarationSyntax classDeclaration = null; if (SyntaxNodeHelper.TryGetParentSyntax(node, out classDeclaration)) { if (excludedClasses.Contains(classDeclaration.Identifier.Text)) { return true; } } } } return false; }
      
      





 /// <param name="excludedProjects">Exclude method declarations that using in excluded projects in current Solution</param> private bool ContainInProjects(IEnumerable<ReferencedSymbol> usingReferences, List<Microsoft.CodeAnalysis.Project> excludedProjects) { if (excludedProjects.Count <= 0) { return false; } foreach (var reference in usingReferences) { if (excludedProjects.FirstOrDefault(p => reference.Locations.FirstOrDefault(l => l.Document.Project.Id == p.Id) != null) != null) { return true; } } return false; }
      
      





そんな美しさを打ち出します。









おわりに



このプロジェクトは、スタジオvsixの拡張として設計することも、たとえばバージョン管理サーバーに配置してアナライザーとして使用することもできます。 また、必要に応じてTuluとして実行できます。







プロジェクト全体がgithubで公開されています。








All Articles