C# 6.0 완벽 가이드/ Roslyn 컴파일러

  • C# 6.0에는 완전히 C#으로 작성된 새로운 컴파일러가 있다. 새 컴파일러는 모듈식으로 구성되어 있어서, 소스 코드를 실행 파일이나 라이브러리로 컴파일하는 것 말고도 그 기능들을 다양한 방식으로 활용할 수 있다. Roslyn(로즐린)이라는 이름의 컴파일러 덕분에 정적 코드 분석 도구나 리팩터링 도구, 구문 강조 기능과 코드 완성 기능을 갖춘 편집기, 그리고 C# 코드를 이해하는 Visual Studio 플러그인을 만들기가 좀 더 쉬워졌다.
    • Roslyn 라이브러리들은 NuGet에서 내려받을 수 있다. C#용 패키지뿐만 아니라 VB용 패키지도 있다. 두 언어는 일부 구조를 공유하므로, 의존하는 라이브러리들도 일부 겹친다. C# 컴파일러 라이브러리들의 NuGet 패키지 ID는 Microfost.CodeAnalysis.CSharp이다.
    • Roslyn의 소스 코드는 Apache 2 오픈소스 사용권 하에 공개되어 있다. 이 소스 코드는 또 다른 가능성을 열어주는데, 예컨대 C#을 커스텀 언어 또는 영역 국한 언어(domain-specific language)로 바꾸는 것도 가능하다. 소스코드는 GitHub의 Roslyn 페이지(https://github.com/dotnet/roslyn)에서 내려받을 수 있다.
    • GitHub의 Roslyn 페이지에는 문서화와 예제들 그리고 코드 분석과 리팩터링 방법을 보여주는 단계별 튜토리얼들이 있다.
  • Roslyn C# 컴파일러 라이브러리를 구성하는 어셈블리들은 다음과 같다.
    • Microsoft.CodeAnalysis.dll
    • Microsoft.CodeAnalysis.CSharp.dll
    • System.Collections.Immutable.dll
    • System.Reflection.Metadata.dll

Roslyn의 구조

  • Roslyn은 컴파일 과정은 다음 세 단계로 나누어서 진행한다.
    1. 코드를 구문 트리로 파싱한다. 이 단계는 구문층(syntatic layer)에 해당한다.
    2. 식별자들을 기호(symbol)들에 묶는다(바인딩). 이 단계는 의미층(semantic layer)에 해당한다.
    3. IL 코드를 산출한다.
  • 첫 단계에서 파서는 C# 코드를 읽어서 구문 트리(syntax tree)를 출력한다. 구문 트리는 소스 코드의 구조와 내용을 트리 형태로 구성한 DOM(Document Object Model; 문서 객체 모형)이다.
  • 둘째 단계에서는 C#의 정적 바인딩(static binding)이 일어난다. 이 단계에서 컴파일러는 어셈블리 참조 정보를 마련해서, 이를테면 ‘Console’ 이라는 식별자가 mscorlib.dll의 System.Console을 지칭한다는 사실을 파악한다. 중복적재 해소와 형식 추론도 이 단계에서 일어난다.
  • 셋째 단계는 출력 어셈블리를 만들어 낸다. 독자가 코드 분석이나 리팩터링을 위해 Roslyn을 사용할 계획이라면 이 셋째 단계는 필요하지 않을 것이다.

작업 영역

  • 이번 장은 새 Roslyn 컴파일러와 그 기능들을 설명한다. 그런데 컴파일러 위에는 작업 영역(workspace)이라고 부르는 또 다른 ‘계층’이 존재한다는 점을 알아둘 필요가 있다. 작업 영역 계층 라이브러리 역시 NuGet에서 얻을 수 있다. 패키지 ID는 Microsoft.CodeAnalysis.CSharp.Workspaces이다.
  • 작업 영역 계층은 Visual Studio 솔루션, 프로젝트, 문서를 인식하며, 엄밀히 말해 컴파일 과정과 직접 연결된 것은 아닌 추가적인 서비스들(이를테면 코드 리팩터링)도 제공한다.
  • 작업 영역 계층 라이브러리는 오픈 소스이며, 소스 코드를 살펴보면 컴파일 계층에 관해 좀 더 많은 것을 배울 수 있다.

구문 트리

  • 구문 트리는 소스 코드를 서술하는 DOM이다. 구문 트리 API는 8장의 ‘표현식 트리’에서 설명한 System.Linq.Expressions API와는 완전히 개별적인 API이다.  두 API에 개념적으로 비슷한 점이 몇 가지 있긴하다. 특히 두 API 모두 C# 표현식을 DOM으로 표현할 수 있다.
  • 그러나 Roslyn의 구문 트리에는 다음과 같은 고유한 특징이 있다.
    • C# 표현식뿐만 안리ㅏ C# 언어 전체를 표현할 수 있다.
    • 주석과 공백(빈칸, 줄바꿈, 탭 등) 그리고 기타 ‘부수 요소’를 포함할 수 있으며, 구문 트리로부터 소스 코드를 원래 모습 그대로 충실하게 복원할 수 있다.
    • 소스 코드를 구문 트리로 파싱하는 ParseText 메서드를 제공한다.
  • 한편 System.Linq.Expressions API의 고유한 특징은 다음과 같다.
    • .NET Framework에 통합되어 있다. 특히 C# 컴파일러 자체가 Expression<T>로 배정 변환되는 람다식을 System.Linq.Expression 형식들을 산출하도록 만들어져 있다.
    • 대리자를 산출하는, 빠르고 가벼운 Compile 메서드를 제공한다. 반면 Roslyn 구문 트리를 컴파일하는 의미층은 완전한 프로그램 전체를 하나의 어셈블리로 컴파일하는 무거운 수단만 제공한다.
  • 두 API의 또 다른 공통점은 구문 트리가 불변이(immutable)라는 점이다. 즉, 일단 트리를 생성하고 나면 트리의 노드들은 변경할 수 없다.
    • 이 때문에 Visual Studio나 LINQPad 같은 응용 프로그램은 편집기에서 사용자가 키를 누를 때마다 구문 트리를 다시 생성해서 구문 강조와 자동 완성 서비스를 갱신한다.
    • 다행히 이러한 잦은 갱신의 비용은 생각보다 높지 않다. 새 구문 트리를 ㅁ나들 때 기존 트리의 요소들을 대부분 재활용할 수 있기 때문이다.
    • 그리고 어떤 객체를 변경할 수 없다는 가정이 있으면 그 객체를 다루는 API를 좀 더 간단하게 만들 수 있다.
    • 또한 이러한 불변이성 덕분에 여러 스레드가 자물쇠 없이도 구문 트리의 모든 부분에 안전하게 접근할 수 있어서 병렬화가 더 쉬워지고 빨라진다.

SyntaxTree 구조체

  • SyntaxTree 구조체로 대표되는 구문 트리의 주된 구성요소는 다음 세 가지이다.
    • 노드(추상 SyntaxNode 클래스)
      • 노드는 표현식, 문장, 메서드 선언 같은 C# 코드 구축 요소를 나타낸다. 모든 노드에는 항상 적어도 하나의 자식 노드가 있다. 즉, 노드가 트리의 잎(leaf; 말단 노드)이 되는 일은 없다. 노드의 자식은 또 다른 노드일 수도 있고 토큰일 수도 있다.
    • 토큰(SyntaxToken 구조체)
      • 토큰은 소스 코드에 쓰이는 식별자, 키워드, 연산자, 문장부호를 나타낸다. 토큰은 자식이 없거나, 있다면 반드시 선행/후행 부소 요소이다. 토큰의 부모는 항상 노드이다.
    • 부수 요소(SyntaxTrivia 구조체)
      • 공백, 주석, 전처리기 지시문, 그리고 조건부 컴파일 때문에 비활성화된 코드를 통칭해서 부수 요소(trivia)라고 부른다. 부수 요소는 항상 바로 왼쪽(선행) 또는 오른쪽(후행)에 있는 토큰과 연결되며, 각각 토큰의 LeadingTrivia 속성 또는 TrailingTrivia 속성에 담긴다.
  • 아래 그림은 다음 코드의 구문 트리를 나타낸 것으로 검은 칸은 노드, 회색 칸은 토큰, 흰 칸은 부수 요소이다.
Console.WriteLine("Hello");

  • SyntaxNode는 추상 클래스이다. C#용 Rosyln 라이브러리에는 C#의 구문 요소마다 개별적인 파생 클래스가 있다. 이를테면 VariableDecalarationSyntax나 TryStatementSyntax가 그러한 SyntaxNode 파생 클래스이다.
  • SyntaxToken과 SyntaxTrivia는 구조체이다. 즉, 이 두형식이 모든 종류의 토큰과 부수 요소를 대표한다. 토큰이나 부수 요소의 구체적인 종류는 RawKind 속성 또는 Kind 확장 메서드로 표현된다.
  • 시각화 도구를 이용하면 구문 트리를 아주 효과적으로 살펴볼 수 있다. Visual Studio는 디버거와 연동되는 시각화 도구를 제공하며(따로 내려받아야 함) LINQPad에는 시각화 기능이 이미 내장되어 있다.
    • LINQPad의 출력 창에 있는 Tree 버튼을 클릭하면 현재 텍스트 편집기에 있는 코드에 대한 구문 트리가 자동으로 시각화 된다.
    • 또한 프로그램 안에서 생성된 구문 트리를 LINQPad로 시각화할 수도 있다. 해당 트리 객체에 대해 DumpSyntaxTree를(또는 노드에 대해 DumpSyntaxNode를) 호출하면 된다.

노드 형식의 이해

  • SyntaxNode의 파생 클래스들은 구문 파싱 결과를 반영하도록 설계되었으며, 그 후에 일어나는 바인딩의 결과로 생기는 의미론적 형식/기호 정보와는 무관하다. 예컨대 다음 코드를 파싱한다고 하다.
using System;

class Foo : SomeBaseClass
{
  void Test() { Console.WriteLine(); }
}
  • Console.WriteLine()이라는 호출 구문이 MethodCallExpressionSyntax 같은 파생 형식의 노드가 될 것 같지만, 그런 클래스는 없다.
    • 대신 이 메서드 호출은 InvocationExpressSyntax 형식의 노드가 되며, 그 노드 아래에는 SimpleMemberAccessExpression 형식의 노드가 있다.
    • 이는 파서가 형식들을 인식하지 못하기 때문이다. 즉, 파서는 Console이 하나의 형식이고 WriteLine이 하나의 메서드라는 점을 알지 못한다. 소스 코드의 ‘구문’만으로는 특정한 하나의 해석을 결정할 수 없다.
    • 예컨대 Console이 SomeBaseClass 의 속성일 수도 있고, WriteLine이 어떤 대리자 형식의 이벤트나 필드, 또는 속성일 수도 있다.
    • 구문에서 알 수 있는 것은, 그 코드가 하나의 멤버 접근(식별자.식별자 형태의) 다음에 인수가 없는 어떤 호출(invocation)이 오는 형태라는 점 뿐이다.

공통의 속성과 메서드

  • 노드와 토큰, 부수 요소에는 공통의 속성들과 메서드들이 있는데, 다음은 그 중 중요한 것들이다.
  • SyntaxTree 속성
    • 객체가 속한 구문 트리를 돌려준다.
  • Span 속성
    • 소스 코드 안에서 객체의 위치를 돌려준다
  • Kind 확장 메서드
    • 노드, 토큰, 부수 요소의 구체적인 종류를 나타내는 SyntaxKind 열거형의 값을 돌려준다. 이 열거형에는 IntKeyword, CommaToken, WhitespaceTrivia 등 수백 가지 값이 정의되어 있다.
    • 노드, 토큰, 부수 요소 모두 동일한 SyntaxKind 열거형을 사용한다.
  • ToString 메서드
    • 노드나 토큰, 부수 요소의 텍스트(소스 코드)를 돌려준다. 토큰의 경우 이 메서드는 Text 속성과 같은 문자열을 돌려준다.
  • GetDiagnostics 메서드
    • 파싱 도중 발생한 오류나 경고 메시지들을 돌려준다.
  • IsEquivalentTo 메서드
    • 현재 객체가 주어진 노드나 토큰, 부수 요소 객체와 동일하면 true를 돌려준다. 비교시 공백 문자도 고려한다(공백 문자를 무시하려면 비교 전에 NormalizeWhitespace를 호출해야 한다)
  • 노드와 토큰에는 FullSpan 속성과 ToFullString 메서드도 있다. 이들은 부수 요소들을 고려하지만, Span과 ToString은 고려하지 않는다.
  • Kind 확장 메서드는 int 형식의 RawKind 속성을 Microsoft.CodeAnalysis.CSharp.SyntaxKind 형식으로 캐스팅해서 돌려주는 단축 수단이다.
    • 그냥 SyntaxKind 형시그이 Kind 속성을 두지 않는 이유는, 토큰 형식들과 부수 요소 형식들이 VB 구문 트리에도 쓰이는데 VB 구문 트리의 SyntaxKind 속성은 C#과는 다른 열거형이기 때문이다.

구문 트리 얻기

  • CSharpSyntaxTree의 정적 메서드 ParseText는 주어진 C# 코드를 파싱해서 SyntaxTree를 만든다.
SyntaxTree tree = CSharpSyntaxTree.ParseText(@"class Test 
{
  static void Main() => Console.WriteLine(""Hello"");
}");
Console.WriteLine(tree.ToString());
tree.DumpSyntaxTree();  // LINQPad dㅔ서 구문 트리 시각화 창이 나타난다
  • 이 예제를 Visual Studio에서 실행하려면 Microsoft.CodeAnalysis.CSharp NuGet 패키지를 설치하고 다음 이른 공간들을 도입해야 한다.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
  • ParseText에는 다양한 중복적재 버전이 있는데, 예컨대 CSharpParseOptions 객체를 전달해서 C# 언어의 특정 버전이나 전처리기 기호를 지정할 수도 있다. 또한 DocumentationMode 형식의 인수를 이용해서 XML 주석의 파싱 여부를 지정할 수도 있다.
    • 소스 코드의 종류를 나타내는 SourceCodeKind 형식의 인수를 받는 버전도 있다. 이 인수로 Interactive나 Script를 지정하면 파서는 프로그램 전체가 아니라 하나의 표현식 또는 문장(들)을 받아들인다. 단, 현재 버전의 Roslyn 라이브러리는 Interactive나 Script를 지정하면 NotSupportedException(아직 지원하지 않는 기능임을 뜻하는 예외)을 던진다.

표현식과 문장의 파싱

  • 프로그램 전체가 아니라 특정 표현식이나 문장(들)만 파싱하는 기능이 Microsoft.CodeAnalysis.CSharp에 들어 있긴 하지만, 아직 완전히 파악되지 않은 시나리오들이 남아 있어서(이를테면 await 표현식이 그렇다) 현재 그 기능은 비활성화 되어 있다. 만일 이 기능을 시험해 보고 싶다면, 다음 두 방법 중 하나를 이용하면 된다.
    • GitHub에서 Roslyn 소스 코드를 내려받아서 CSharpParseOptions.cs의 관련 점검 코드를 비활성화한 후 솔루션을 다시 빌드한다.
    • CSharpParseOptions 인스턴스를 생성한 후 반영 기능을 이용해서 SourceCOdeKind를 Interactive나 Script로 설정한다.
  • LINQPad는 ‘Language’가 Expression이나 Statements로 선택된 상태에서 구문 트리를 표시할 때 두 번째 방법을 이용한다.
  • 구문 트리를 얻는 또 다른 방법은 노드들과 토큰들로 이루어진 객체 그래프를 인수로 해서 CSharpSyntaxTree.Create를 호출하는 것이다.
  • 일단 구문 트리를 얻었다면, 트리에 대해 GetDiagnostics 메서드를 호출해서 파싱 과정에서 발생한 오류들과 경고들을 알아낼 수 있다. (이 메서드를 특정 노드나 토큰에 대해 호출해도 된다)
  • 파싱에서 예기치 않은 오류가 발생했다면, 트리가 예상과는 다른 구조일 수도 있다. 따라서 트리를 본격적으로 사용하기 전에 먼저 GetDiagnostics를 호출해서 오류를 점검하는 것이 바람직하다.
  • 한 가지 멋진 기능은 오류가 있는 트리를 다시 원래의 텍스트(동일한 오류들이 있는)로 바꿀 수 있다는 것이다. 그런 경우 파서는 의미층에 유용한 구문 트리를 제공하기 위해 최선을 다하며, 필요하다면 ‘유령 노드(phantom node)’들도 생성한다. 불완전한 코드에 대해서도 코드 완성 기능이 작동하는 것은 파서의 이러한 처리 덕분이다.(노드가 유령 노드인지는 IsMissing 속성을 보면 알 수 있다)
  • 이전 절의 예제에서 생성한 구문 트리에 GetDiagnostics를 호출하면 아무런 오류도 나오지 않는다. 사실 그 예제에는 오류가 있다. 바로 System 이름공간을 도입하지 않고도 Console.WriteLine을 호출한다는 것이다.
    • 이는 구문 파싱과 의미 파싱의 차이를 잘 보여준다. 구문만 따지면 그 예제는 올바르다. 방금 말한 오류는 컴파일 후 어셈블리 참조들을 추가하고 의미 모형(semantic model)을 질의해서 바인딩이 일어나야 비로소 드러난다.

트리의 운행과 검색

  • SyntaxTree는 트리 구조를 감싸는 래퍼(wrapper)로 작용한다. SyntaxTree에는 하나의 뿌리(루트) 노드에 대한 탐조가 있다. GetRoot 메서드를 호출하면 그 참조가 반환된다.
var tree = CSharpSyntaxTree.ParseText(@"class Test
{
  static void Main => Console.WriteLine(""Hello"");
}");

SyntaxNode root = tree.GetRoot();
  • C# 프로그램 구문 트리의 뿌리 노드는 CompilationUnitSyntax 형식의 객체이다.
Console.WriteLine(root.GetType().Name);  // CompliationUnitSyntax

자식 객체 운행

  • SyntaxNode는 자식 노드들과 자식 토큰들을 운행(traversal)하는데 사용할 수 있는 여러 LINQ 친화적 메서드를 제공한다. 가장 간단한 메서드들은 다음과 같다.
IEnuemrable<SyntaxNode> ChildNodes()
IEnumerable<SystaxToken> ChildTokens()
  • 앞의 예제를 이어서 뿌리 노드에는 클래스 선언을 나타내는 ClassDeclarationSyntax 형식의 자식 노드 하나가 있다.
var cds = (ClassDeclarationSyntax) root.ChildNodes().Single();
  • 이 cds의 멤버들을 ChildNodes 메서드로 구할 수도 있고, 다음처럼 ClassDelcarationSyntax의 Members 속성으로 열거할 수 있다.
foreach(MemberDeclarationSyntax member in cds.Members)
  Console.WriteLine(member.ToString());
  • 결과는 다음과 같다.
static void Main => Console.WriteLine("Hello");
  • 또한 더 깊게 내려가면서 자식들을 재귀적으로 운행한느 Descendant* 메서드들도 있다. 다음은 프로그램을 구성하는 모든 토큰을 DescendantTokens 메서드를 이용해서 열거하는 예이다.
foreach(var token in root.DescendantTokens())
  Console.WriteLine($"{token.Kind(), -30} {token.Text}");
  • 결과는 다음과 같다.
ClassKeyword			        class
IdentifierToken			Test
OpenBraceToken			{
StaticKeyworkd			static
VoidKeyword				void
IdentifierToken			Main
OpenParenToken			(
CloseParenToken			)
EqualsGreaterThanToken	=>
IdentifierToken			Console
DotToken				.
OpenParenToken			(
StringLiteralToken		       "Hello"
CloseParenToken			)
SemicolonToken			;
CloseBraceToken			{
EndOfFileToken
  • 이 결과에 공백이 하나도 없음을 주목하기 바란다. token.Text를 token.ToFullString()으로 대체하면 공백(그리고 기타 모든 부수 요소)도 결과에 포함된다.
  • 다음 예제는 DescendantNodes를 이용해서 메서드 선언에 해당하는 구문 트리 노드를 찾는다.
var outMethod = root.DescendantNodes().First(m => m.Kind() == SyntaxKind.MethodDeclaration);
  • 또는 다음과 같이 해도 된다.
var outMethod = root.DescendantNodes().OfType<MethodDeclarationSyntax>().Single();
  • 후자에서 ourMethod는 MethodDeclarationSyntax 형식의 객체이다. MethodDeclarationSyntax는 메서드 선언과 관련된 여러 유용한 속성을 제공한다. 예컨대 메서드 선언이 여러 개인 C# 프로그램의 구문 트리를 생성했다고 할 떄, 다음 코드는 이름이 ‘Main’인 메서드의 선언을 찾는다.
var outMethod = root.DescendantNodes().OfType<MethodDeclarationSyntax>().Single(m => m.Identifier.Text == "Main");
  • MethodDeclarationSyntax의 Identifier 속성은 메서드의 식별자(즉, 메서드 이름)에 해당하는 토큰을 돌려준다. 이 속성을 사용하지 않고 같은 결과를 얻으려면 다음과 같이 좀 더 복잡한 코드가 필요하다.
root.DescendantNodes().First(m => 
  m.Kind() == SyntaxKind.MethodDeclaration &&
  m.ChildTokens().Any(t =>
    t.Kind() == SyntaxKind.IdentifierToken && t.Text == "Main"));
  • SyntaxNode에는 또한 GetFirstToken과 GetLastToken 메서드가 있다. 이들의 반환값은 각각 DescendantTokens().First()와 DescendantTokens().Last()의 반환값과 같다.
  • DescendantTokens().Last() 보다 GetLastToken()이 빠르다. 전자는 모든 후손을 열거해서 마지막 토큰을 찾지만, 후자는 마지막 토큰을 직접 돌려주기 때문이다.
  • 하나의 노드의 자식 객체는 노드일 수도 있고 토큰일 수도 있으며, 또한 그 자식 노드들과 토큰들의 상대적 순서가 중요하므로, SyntaxNode는 자식 노드들과 자식 토큰들을 함께 열거하기 위한 메서드들도 제공한다.
ChildSyntaxList ChildNodesAndTokens()
IEnumerable<SyntaxNodeOrToken> DescendantNodesAndTokens()
IEnumerable<SyntaxNodeOrToken> DescendantNodesAndTokensAndSelf()
  • ChildSyntaxList는 IEnumerable<SyntaxNodeOrToken>를 구현할 뿐만 아니라, 자식 객체 개수를 담은 Count 속성과 위치(색인)로 요소에 접근할 수 있는 인덱서도 제공한다.
  • 노드의 부수 요소들을 GetLeadingTrivia, GetTrailingTrivia, DescendantTrivia 메서드를 이용해서 직접 운행할 수 있지만, 부수 요소가 부착된 토큰의 LeadingTrivia 속성과 TrailingTrivia 속성을 이용해서 운행하는 것이 더 일반적이다.
    • 또는 노드로부터 텍스트(소스 코드)를 얻는 경우 ToFullString 메서드를 호출하면 결과에 부수 요소들도 포함된다.

부모 운행

  • 노드와 토큰에는 SyntaxNode 형식의 Parent 속성이 있다.
  • 부수 요소(SyntaxTrivia)의 경우 ‘부모’는 토큰이며, Token 속성을 통해서 그 토큰에 접근할 수 있다.
  • 노드에는 또한 트리를 따라 위로 올라가는 메서드들도 있다. 그런 메서드들은 이름이 “Ancestor”로 시작한다.

위치로 자식 객체 찾기

  • 모든 노드와 토큰, 부수 요소에는 TextSpan 형식의 Span 속성이 있다. 이 속성은 소스 코드 안에서 해당 요소가 차지한 구간(span)의 위치와 크기를 알려주는 문자 오프셋들을 제공한다.
    • 노드와 토큰에는 FullSpan이라는 속성도 있는데, 이 속성은 해당 객체의 선행(leading) 부수 요소와 후행(trailing) 부수 요소도 포함한다.
    • 반면 Span은 부수 요소들을 고려하지 않는다. 단, 노드의 Span은 자식 노드들과 토큰들을 포함한다.
  • SyntaxNode의 FindNode, FindToken, FindTrivia 메서드를 이용하면 노드의 특정 자식 노드나 자식 토큰, 자식 부수 요소를 위치를 이용해서 찾을 수 있다.
    • 이 메서드들은 인수로 주어진 구간을 완전히 포함하는 가장 작은 구간을 가진 후손 요소를 돌려준다. 또한 후손 노드들과 후손 토큰들을 함께 검색하는 ChildThatContainsPosition이라는 메서드도 있다.
  • 후손 노드를 검색했을 때 구간 길이가 같은 두 노드(흔히 자식 노드와 자식 노드의 자식 노득)가 나오면, FindNode 메서드는 바깥쪽(부모 쪽) 노드를 돌려준다. 단, 호출 시 선택적 매개변수 getInnermostNodeForTie에 true를 지정하면 안쪽 노드가 반환된다.
  • Find* 메서드들에는 fndInsideTrivia라는 bool 형식의 선택적 매개변수도 있다. 여기에 true를 지정하면 구조적 부수 요소 안에서 노드나 토큰을 찾는다.

TextSpan 다루기

  • TextSpan 구조체에슨 Start, Length, End라는 정수 속성들이 있다. 이들은 소스 코드 안의 한 문자 구간의 시작 오프셋과 길이, 끝 오프셋(모두 문자 단위)에 해당한다. 이외에 Overlap, OverlapWith, Intersection, IntersectsWith라는 속성들도 있다.
    • 중첩(overlap)과 교차(intersection)의 차이는 문자 하나이다. 만일 한 구간이 끝나기 전에 다른 구간이 시작하면 중첩(<)이고, 두 구간이 맞당아 있으면 교차(<=)이다.
  • SyntaxTree 클래스는 TextSpan 객체를 행 번호와 문자 오프셋으로 변환해 주는 GetLineSpan이라는 메서드를 제공한다. 이 메서드는 소스 코드에 있는 모든 #line 지시자의 효과를 무시한다. GetMappedLineSpan 메서드는 변환 시 그 지시자들의 효과를 반영한다.

CSharpSyntaxWalker 클래스

  • CSharpSyntaxWalker의 파생 클래스를 만들고 CSharpSyntaxWalker의 수백 가상 메서드 중 몇 개를 재정의해서 구문 트리를 운행할 수도 있다. 다음 클래스는 if 문의 개수를 센다.
class IfCounter : CSharpSyntaxWalker
{
  public int IfCount { get; private set; }

  public override void VisitIfStatement (IfStatementSyntax node)
  {
    IfCount++;
    // 자식들로 더 내려 가기 위해 기반 클래스의 메서드를 호출한다.
    base.VisitIfStatement(node);
  }
}
  • 다음은 이 클래스의 사용법을 보여 주는 예제이다.
var ifCounter = new IfCount();
ifCounter.Visit(root);
Console.WriteLine($"if 문 {ifCounter.IfCount} 개 발견");
  • 다음 코드로도 같은 결과를 얻을 수 있다.
root.DescendantNodes().OfType<IfStatementSyntax>().Count()
  • 지금 예제는 그렇지 않지만, 좀 더 복잡한 경우에서는 CSharpSyntaxWalker의 파생 클래스를 만들어서 여러 메서드를 재정의하는 것이 Descendant* 메서드들을 사용하는 것보다 쉬울 수 있다(부분적으로 이는 C#에 F# 같은 패턴 부합 라이브러리가 없기 때문이다.)
  • 기본적으로 CSharpSyntaxWalker는 노드들만 방문한다. 토큰이나 부수 요소를 방문하려면 SyntaxWalkerDepth 열거형으로 적절한 깊이를 지정해서 기반 생성자를 호출해야 한다.
    • 여기서 깊이는 노드->토큰->부수 요소 순이다. 그리고 VisitToken과 VisitTrivia를 재정의하면 된다.
class WhiteWalker : CSharpSyntaxWalker // 공백 문자들을 센다
{
  public int SapceCount { get; private set; }

  public WhiteWalker() : base (SyntaxWalkerDepth.Trivia) { }

  public override void VisitTrivia (SyntaxTrivia trivia)
  {
    SpaceCount += trivia.ToString().Count(char.IsWhiteSpace);
    base.VisitTrivia(trivia);
  }
}
  • WhiteWalker에서 기반 생성자 호출을 제거하면 VisitTrivia 콜백이 호출되지 않는다.

부수 요소

  • 파싱된 구문 트리로부터 출력 어셈블리를 생성하는 과정에서 컴파일러가 완전히 무시하는 요소들이 바로 부수 요소(trivia)이다. 구체적으로 공백, 주석, XML 문서, 전처리 지시문, 그리고 조건부 컴파일 때문에 비환성화된 코드가 부수 요소로 간주된다.
  • 그런데 코드에 꼭 필요한 공백도 부수 요소로 간주된다. 그런 공백은 파싱에는 꼭 필요하지만, 일단 구문 트리가 ㅁ나들어진 후에는 더 이상 필요하지 않다(적어도 컴파일러에게는 필요가 없다) 그러나 원래의 소스 코드를 다시 복원하는데에는 부수 요소가 중요하다.
  • 구문 트리 안에서 부수 요소는 인접 토큰에 속한다. 관례상, 파서는 한 토큰 다음에 나오는 모든 공백과 주석(현재 행의 끝까지)을 그 토큰의 후행 부수 요소로 둔다. 그리고 그 다음의 모든 부수 요소는 그 다음 토큰의 선행 부수 요소가 된다. (파일의 시작과 끝에서는 예외 규칙이 있다.)
    • 토큰을 프로그램 안에서 생성할 때에는 공백을 토큰 앞에 두 ㄹ수도 있고 뒤에 둘 수도 있다.(소스 코드로 다시 변환할 것이 아니라면 공백을 아예 두지 않아도 된다.)
var tree = CSharpSyntaxTree.ParseText(@"class Program
{
  static /*주석*/ vois Main() { }
}");

SyntaxNode root = tree.GetRoot();

// static 키워드 토큰을 찾는다.
var method = root.DescendantTokens().Single(t => t.Kind() == SyntaxKind.StaticKeyword);

// static 키워드 토큰 주변의 부수 요소들을 출력한다.
foreach (SyntaxTrivia t in method.LeadingTrivia)
  Console.WriteLine(new { Kind = "Leading " + t.Kind(), t.Span.Length });

foreach (SyntaxTrivia t in method.TrailingTrivia)
  Console.WriteLine(new { Kind = "Trailing " + t.Kind(), t.Span.Length });
  • 출력은 다음과 같다.
{ Kind = Leading WhitespaceTrivia, Length = 1 }
{ Kind = Trailing WhitespaceTrivia, Length = 1 }
{ Kind = Trailing MultiLineCommentTrivia, Length = 6 }
{ Kind = Trailing WhitespaceTrivia, Length = 1 }

전처리기 지시문

  • 전처리기 지시문은 컴파일 결과에 사소하지 않은 영향을 미치므로, 전처리기 지시문을 부수 효과로 간주하는 것이 좀 이상할 수도 있겠다.
  • 전처리기 지시문을 부수 요소로 치는 것은 이들의 의미를 실제 컴파일 과정 이전에 파서 자체가 처리하기 때문이다. 즉, 전처리는 파서의 몫이다. 일단 파싱이 끝남녀, 컴파일러가 명시적으로 고려해야 할 전처리기 지시문은 없다(단, #pragma는 예외). 이 점을 보여주는 예로 다음과 같은 조건부 컴파일 지시문들을 파서가 어떻게 처리하는지 살펴보자.
#define FOO

#if FOO
  Console.WriteLine("FOO가 정의되어 있음");
#else
  Console.WriteLine("FOO가 정의되어 있지 않음");
#endif
  • #if Foo에 도달한 파서는 FOO가 정의되어 있으므로 그 다음 행을 정상적으로 파싱한다(노드들과 토큰들이 생성된다). 그리고 #else 지시자 다음의 코드는 DisabledTextTrivia로 만든다.
    • CSharpSyntaxTree.Parse를 호출하고 난 후, CSharpParseOptions 인스턴스를 이용해서 추가적인 전처리기 기호들을 구문 트리에 넣을 수 있다.
  • 이처럼 조건부 컴파일 지시문들의 효과는 파싱 과정에서 발휘될 뿐이며, 일단 파싱이 끝나면 지시문 자체와 비활성 코드는 컴파일러가 완전히 무시할 수 있는 텍스트이다. 따라서 이들은 부수 효과로 취급해야 마땅하다.
  • #line 지시문 역시 파서가 읽어서 직접 처리한다. 이 지시문에 담긴 정보는 구문 트리에 대해 GetMappedLineSpan이 호출되었을 때 쓰인다.
  • #region 지시문은 의미론적으로 빈 지시문이다. 파서가 하는 유일한 역할은 #region 지시자에 대응되는 #endregion 지시자가 있는지 보는 것뿐이다. #error와 #warning 역시 파서가 처리한다. 트리 또는 노드에 대해 GetDiagnostics를 호출하면 이들이 생성한 오류와 경고를 얻을 수 있다.
  • 출력 어셈블리 산출 이외의 목적으로는 전처리기 지시문의 내용을 조사하는 것이 여전히 유용할 수 있다(이를테면 구문 강조 등). 그런 경우 구조적 부수 요소가 유용하다.

구조적 부수 요소

  • 부수 요소는 두 종류로 나뉜다.
    • 비구조적 부수 요소
      • 주석, 공백, 그리고 조건부 컴파일 때문에 비활성화된 코드가 여기에 속한다.
    • 구조적 부수 요소
      • 전처리기 지시문과 XML 문서화가 여기에 속한다.
  • 비구조적 부수 요소(unstructured trivia)는 그냥 텍스트로 취급되지만, 구조적 부수 요소(structured trivia)는 그 내용이 소형 구문 트리(miniature syntax tree)로 파싱된다.
  • SyntaxTrivia의 HasStructure 속성은 현재 부수 요소 객체가 구조적 부수 요소인지의 여부를 돌려준다. 그리고 GetStructure 메서드는 부수 요소의 소형 구문트리를 돌려준다.
var tree = CSharpSyntaxeTree.ParseText(@"#define FOO");

// LINQPad에서:
tree.DumpSyntaxTree();  // LINQPad가 구조적 부수 요소의 시각화 창을 표시한다.

SyntaxNode root = tree.GetRoot();

var trivia = root.DescendantTrivia().First();
Console.WriteLine(trivia.HasStructure);  // True
Console.WriteLine(trivia.GetStructure().Kind());  // DefineDirectiveTrivia
  • 전처리기 지시문들은 SyntaxNode에 대해 GetFirstDirective를 호출해서 직접 열거할 수 있다. 또한 주어진 노드에 전처리기 지시문 부수 요소가 포함되어 있는지를 알려주는 ContainsDirectives 속성도 있다.
var tree = CSharpSyntaxeTree.ParseText(@"#define FOO");

SyntaxNode root = tree.GetRoot();

Console.WriteLine(root.ContainsDirectives);  // True

// directive는 구조적 부수 요소의 뿌리 노드이다.
var directive = root.GetFirstDirective();
Console.WriteLine(directive.Kind());  // DefineDirectiveTrivia
Console.WriteLine(directive.ToString());  // #define FOO

// 지시문이 더 있다면, GetNextDirective를 이용해서 얻을 수 있다.
Console.WriteLine(directive.GetNextDirective());  // (null)
  • 다른 트리 요소 객체들과 마찬가지로 일단 부수 요소 객체를 얻은 후에는 그것을 좀 더 구체적인 형식으로 캐스팅해서 속성들을 조회할 수 있다.
var hashDefine = (DefineDirectiveTriviaSyntax) root.GetFirstDirective();
Console.WriteLine(hashDefine.Name.Text);  // FOO
  • 모든 노드와 토큰, 부수 요소 객체에는 IsPartOfStrucutredTrivia라는 속성이 있다. 이 속성은 해당 객체가 구조적 부수 요소 소형 구문 트리의 일부인지(즉, 부수 요소 객체의 후손인지)의 여부를 나타낸다.

구문트리의 변형

  • 구문 트리의 노드와 토큰, 부수 요소를 ‘수정’하는 것도 가능하다. 이를 위한 메서드들은 이름이 다음과 같은 접두사들로 시작한다. (이 메서드들은 대부분 확장 메서드이다)
    • Add*
    • Insert*
    • Remove*
    • Replace*
    • With*
    • Without*
  • 그런데 구문 트리는 불변이(immutable)이므로, 이 메서드들은 모두 원래의 객체를 그대로 두고, 요청된 변경 사항을 반영한 새 객체를 돌려준다.

소스 코드 변경 반영

  • 예컨대 C# 편집기를 작성한다면, 소스 코드의 변경에 기초해서 구문 트리를 갱신해야 한다. 그런 상황에 딱 맞는 메서드가 SyntaxTree 클래스의 WithChangedText이다. 이 메서드는 SourceText(Microsoft.CodeAnalysis.Text 이름공간에 있다) 객체로 표현된 소스 코드 변경 사항에 기초해서 소스 코드를 부분적으로 다시 파싱한다.
  • SourceText 객체를 생성하는 한 가지 방법은 완전한 소스 코드를 인수로 해서 SourceText의 정적 메서드 From 을 호출하는 것이다. 그런 다음 그 객체로 구문 트리를 생성할 수 있다.
SourceText sourceText = SourceText.From("class Program {}");
var tree = CSharpSyntaxTree.ParseText(sourceText);
  • 또는 기존 트리에 대해 GetText를 호출해서 SourceText 객체를 얻을 수도 있다.
  • 어떤 방법으로든 SourceText 객체를 마련했다면, Replace나 WithChanges 메서드로 소스 코드의 일부를 변경한다. 앞의 예제를 이어서 다음은 코드의 처음 다섯 글자(“class”)를 “struct”로 고치는 예이다.
var newSource = sourceText.Replace(0, 5, "struct");
  • 이제 변경된 SourceText 객체로 WithChangedText를 호출해서 구문 트리를 갱신한다.
var newTree = tree.WithChangedText(newSource);
Console.WriteLine(newTree.ToString());  // struct Program {}

SyntaxFactory를 이용해서 새 노드, 토큰, 부수 요소 생성

  • SyntaxFactory 클래스는 텍스트(소스 코드) 없이 노드, 토큰, 부수 요소를 직접 생성하는 여러 정적 메서드를 제공한다. 그런 식으로 생성한 노드, 토큰, 부수 요소는 기존 구문 트리를 ‘변형’ 하거나 새 구문 트리를 명시적으로 생성하는데 사용할 수 있다.
  • 이런 접근방식에서 가장 어려운 부분은 구체적으로 어떤 종류의 노드와 토큰을 생성해야 하는지 파악하는 것이다.
    • 잘 모르겠다면 먼저 예제 소스 코드 텍스트를 파싱해서 구문 트리를 생성한 후 그것을 구문 트리 가시화 도구로 살펴보면 매우 도움이 된다.
    • 예컨대 다음 코드에 해당하는 구문 트리 노드를 생성한다고 하자.
using System.Text;
  • 다음 코드를 LINQPad에서 실행하면 위의 코드에 대한 구문 트리가 보기 쉬운 형태로 표시된다.
CSharpSyntaxTree.ParseText("using System.Text;").DumpSyntaxTree();
  • “using System.Text;”이 오류 없이 파싱되는 이유는 이 코드가 비록 하는 일은 없지만, 그래도 하나의 완결적인 프로그램이기 때문이다. 그러나 대부분의 예제 코드 조각들은 적당한 메서드나 형식 정의로 감싸주어야 제대로 파싱된다.
  • 출력은 다음과 같다. 우리가 관심을 둘 부분은 둘째 노드(즉, UsingDirective와 그 후손들)이다.
Kind								Token Text
==================================== ===========
CompliationUnit (node)
	UsingDirective (node)
		UsingKeyword (token)			using
			WhitespaceTrivia (trailing)
		QaulifiedName (node)
			IdentifierName (node)
				IdentifierToken (token)	System
			DotToken (token)			.
			IdentifierName (name)
				IdentifierToken (token)	Text
		SemiColonToken (token)		;
	EndOfFileToken (token)
  • 그 노드의 제일 안쪽을 보면 IdentifierName 노드가 두 개 있다. 그리고 두 노드의 부모는 QualifiedName이다 .이 노드를 다음과 같이 생성할 수 있다.
QualifiedNameSyntax qualifiedName = SyntaxFactory.QualifiedName(
  SyntaxFactory.IdentifierName("System"),
  SyntaxFactory.IdentifierName("Text"));
  • 이 코드는 식별자 노드 두 개를 받는 QualifiedName의 중복적재 버전을 사용한다. 이 버전은 마침표 토큰을 자동으로 삽입해 준다.
  • 이제 이 노드를 UsingDirective로 감싼다.
UsingDirectiveSyntax usingDirective = SyntaxFactory.UsingDirective(qualifiedName);
  • “using” 키워드나 후행 세미콜론에 해당하는 토큰들은 지정하지 않았음을 주목하기 바란다. 그러면 SyntaxFactory가 자동으로 그 토큰들을 생성해서 추가한다.
    • 그런데 자동으로 생성된 토큰들에 공백 부수 요소가 포함되지는 않는다. 그 때문에 컴파일에 문제가 생기는 것은 아니지만 구문 트리르르 다시 텍스트로 되돌리면 다음과 같이 유효하지 않은 코드가 나온다.
Console.WriteLine(usingDirective.ToFullString());  //usingSystem.Text
  • 이 문제는 노드(또는 노드의 선조 중 하나)에 대해 NormalizeWhitespace를 호출하면 된다. NormalizeWhitespace를 호출하면 소스 코드 복원 시 공백 부수 요소들(정확한 구문을 위한 것들과 가독성을 위한 것들 모두)이 자동으로 추가된다.
    • 또는 만일 공백들을 좀 더 세밀하게 제어하고 싶다면 다음처럼 명시적으로 ㅜㅊ가할 수도 있다.
usingDirective = usingDirective.WithUsingKeyword(
  usingDirective.UsingKeyword.WithTrailingTrivia (
    SyntaxFactory.Whitespace(" ")));

Console.WriteLine(usingDirective.ToFullString());  // using System.Text;
  • 간결함을 위해 이 코드는 노드의 기존 UsingKeyword를 ‘수확해서’ 거기에 후행 부수 요소를 추가한다. SyntaxFactory.Token(SyntaxKind.UsingKeyword) 호출로도 같은 토큰을 얻을 수 있지만, 코드가 더 길어진다.
  • 마지막으로 할 일은 이 UsingDirective 노드를 기존 구문 트리 또는 새 구문 트리에 (좀 더 정확하게는 구문 트리의 뿌리 노드에) 추가하는 것이다.
    • 기존 트리에 추가하는 경우에는 기존 트리의 뿌리 노드를 CompilationUnitSyntax 형식으로 변환한 후 AddUsings 메서드를 호출하면 된다.
    • 그런 다음에는 변형된 컴파일 단위(CompilationUnitSyntax 객체)를 이용해서 새 구문 트리(기존 트리에 UsingDirective 노드가 추가된)를 생성한다.
var existingTree = CSharpSyntaxree.ParseText("class Program {}");
var existingUnit = (CompilationUnitSyntax) existingTree.GetRoot();
var unitWithUsing = existingUnit.AddUsings(usingDirective);
var treeWithUsing = CSharpSyntaxTree.Create(unitWithUsing.NormalizeWhitespace());
  • 한 구문 트리의 모든 부분이 불변이임을 기억하기 바란다. AddUsings 호출은 새로운 노드를 돌려줄 뿐 기존 노드는 전혀 변경하지 않는다. 이런 메서드 호출의 반환값을 무시하는 우를 범하기 쉬우니 조심해야 한다.
  • 이 예는 컴파일 단위에 대해 NormalizeWhitespace를 호출한다. 이후 트리에 대해 ToString을 호출하면 문법적으로 정확하고 읽기 쉬운 소스 코드가 나온다. 또는 다음처럼 새 줄 부수 요소를 usingDirective에 명시적으로 추가할 수도 있다.
.WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n\r\n"))
  • 완전히 새로운 컴파일 단위와 구문 트리에 노드를 추가하는 방법도 이와 비슷하다. 가장 쉬운 접근방식은 빈 컴파일 단위로 시작해서 앞에서처럼 AddUsings를 호출하는 것이다.
var unit = SyntaxFactory.CompilationUnit().AddUsings(usingDirective);
  • 더 나아가서 형식 정의를 만들어서 추가하는 것도 비슷한 방식으로 진행하면 된다. 단 형식 정의를 추가할 때는 AddMembers 메서드를 사용한다.
// 간단한 빈 클래스 정의를 생성, 추가한다.
unit = unit.AddMembers(SyntaxFactory.ClassDeclaration("Program"));
  • 마지막으로, 지금까지 만든 컴파일 단위를 이용해서 새 구문 트리를 생성한다.
var tree = CSharpSyntaxTree.Create(unit.NormalizeWhitespace());
Console.WriteLine(tree.ToString());

// 출력
// using System.Text;
// class Program
// {
// }

CSharpSyntaxRewriter 클래스

  • 구문 트리를 좀 더 복잡한 방식으로 변형해야 한다면 CSharpSyntaxRewriter의 파생 클래스를 작성해서 적용하는 것도 한 방법이다.
  • CSharpSyntaxRewriter는 이전에 살펴본 CSharpSyntaxWalker 클래스와 비슷하다. 단 Visit* 메서드들이 구문 노드(SyntaxNode)를 받을 뿐만 아니라 돌려주기도 한다는 점이 다르다.
    • 전달된 구문 노드와는 다른 구문 노드를 돌려주면, 결과적으로 구문 트리를 ‘고쳐 쓰는(rewrite)’ 것이 된다.
  • 예컨대 다음의 CSharpSyntaxRewriter 파생 클래스는 선언된 메서드 이름을 대문자로 바꾼다.
class MyRewriter : CSharpSyntaxRewriter
{
  public override SyntaxNode VisitMethodDeclaration (MethodDeclarartionSyntax node)
  {
    // 메서드의 식별자를 대문자 버전으로 '대체한다'
    return node.WithIdentifier (
      SyntaxFactory.Identifier(
        node.Identifier.LeadingTrivia,  // 기존 부수 요소는 유지한다.
        node.Identifier.Text.ToUpperInvariant(),
        node.Identifier.TrailingTrivia));  // 기존 부수 요소는 유지한다.
  }
}
  • 다음은 이 클래스를 사용하는 방법을 보여주는 예이다.
var tree = CSharpSyntaxTree.ParseText(@"class Program
{
  static void Main () { Test(); }
  static void Test () { }
}");

var rewriter = new MyRewriter();
var newRoot = rewriter.Visit(tree.GetRoot());
Console.WriteLine(newRoot.ToFullString());

// 출력
// class Program
// {
//    static void MAIN() { Test(); }
//    static void TEST() { }
// }
  • Main 메서드 안의 Test() 호출은 대문자로 바뀌지 않았음을 주목하기 바란다. 이는 이 파생 클래스가 단지 멤버 선언들만 방문할 뿐, 호출들은 방문하지 않기 때문이다.
    • 호출에 쓰인 이름까지 바꾸려면 Main()이나 Test() 같은 호출이 실제로 Program 형식을(다른 어떤 형식이 아니라) 지칭하는지 점건해야 하는데, 구문 트리만으로는 불가능하다. 의미 모형도 있어야 한다.

컴파일 공정과 의미 모형

  • 하나의 컴파일 공정에는 구문 트리들과 참조들, 그리고 컴파일 옵션들이 쓰인다. 컴파일의 목적은 두 가지이다.
    • 라이브러리나 실행 파일을 만ㄷ르어 낸다(산출(emit) 단계)
    • 기호 정보(바인딩 단계에서 얻은)를 제공하는 의미 모형(semantic model)을 노출한다.
  • 의미 모형은 기호 이름 바꾸기나 편집기의 코드 완성 목록 제공 같은 기능을 구현할 때 꼭 필요하다.

컴파일 객체 생성

  • 목적이 의미 모형을 질의하는 것이 아니면 이진 코드를 산출하는 것이든, 첫 단계는 컴파일 공정을 대표하는 CSharpCompilatoin 객체를 생성하는 것이다. 이때 생성할 어셈블리의 이름(단순명)을 지정해야 한다.
var compilation = CSharpCompilation.Create("test");
  • 어셈블리를 산출하려는 것이 아니어도 어셈블리의 단순명을 꼭 지정해야 한다. 이는 그 이름이 컴파일 객체 안에 있는 형식들을 식별하는데 쓰이기 때문이다.
  • 기본적으로 컴파일 객체는 라이브러리를 출력하도록 설정되어 있다. 다른 종류의 출력(Windows 실행 파일, 콘솔 실행 파일 등)을 원한다면 다음처럼 WithOptions 메서드를 호출해 주어야 한다.
compilation = compilation.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication));
  • 호출의 인수로 쓰인 CSharpCompilationOptions 클래스의 생성자에는 csc.exe 도구의 명령줄 옵션들에 대응하는 십여 개의 선택적 매개변수들이 있다.
    • 예컨대 컴파일러 최적화들을 활성화하고 어셈블리에 강력한 이름을 부여하려면 다음과 같이 하면 된다.
compilation = compilation.WithOptions(
  new CSharpCompilationOptions(OutputKind.ConsoleApplication,
  cryptoKeyFile:"myKeyFile.snk",
  optimizationLevel:OptimizationLevel.Release));
  • 이제 컴파일 객체에 구문 트리들을 추가해보자. 각 구문 트리는 컴파일에 포함되는 ‘파일’에 해당한다.
var tree = CSharpSyntaxTree.ParseText(@"class Program
{
  static void Main() => System.Console.WriteLine(""Hello"");
}");

compilation = compilation.AddSyntaxTrees(tree);
  • 다음으로 할 일은 참조들을 추가하는 것이다. 가장 간단한 C# 프로그램은 mscorlib.dll 하나만 참조한다. mscorlib.dll에 대한 참조를 다음과 같이 추가할 수 있다.
compilation = compilation.AddReferences(MetadataReference.CreateFromFile(typeof(int).Assembly.Location));
  • MetadataReference.CreateFromFile 메서드는 지정된 어셈블리의 내용을 메모리에 읽어들이는데, 보통의 반영 기능을 사용하지 않는다.
    • 대신 이 메서드는 System.Reflection.Metadata라는 고성능의 이식성 있는 어셈블리 판독기를 사용한다(NuGet에서 구할 수 있다)
    • 이 판독기는 부수 효과를 발생하지 않는다. 특히, 어셈블리를 현재 응용 프로그램 도메인에 적재하지 않는다.
  • MetadataReference.CreateFromFile이 돌려주는 PortableExcutableReference 객체는 메모리를 상당히 많이 소비할 수 있으므로, 필요하지 않은 참조들을 너무 오래 유지하지 않도록 하는데 신경을 써야 한다.
    • 또한 같은 어셈블리에 대한 참조를 여러 번 만들어야 한다면 참조를 캐시에 담아 두는 것이 바람직하다(이런 용도로는 약한 참조들을 담는 캐시가 이상적이다)
  • 구문 트리들과 참조들 그리고 옵션들을 받도록 중복적재된 CSharpCompilation.Create를 이용한다면 이상의 모든 것을 한 번의 호출로 수행하는 것도 가능하다.
    • 아니면 다음과 같이 ‘유창한’ 형태의 표현식 하나로 만드는 것도 가능하다.
var compilation = CSharpCompilation.Create("...")
  .WithOptions(...)
  .AddSyntaxTrees(...)
  .AddReferences(...);

진단

  • 구문 트리에 오류가 없어도 그것을 컴파일하면 오류나 경고가 나올 수 있다.
    • 예컨대 필요한 이름공간을 도입하지 않았거나 형식이나 멤버 이름에 오타가 있거나, 매개변수 형식의 추론에 실패하면 오류가 발생한다.
    • 발생한 오류들과 경고들은 컴파일 객체에 대해 GetDiagnostics를 호출해서 얻을 수 있다. 이 메서드가 돌려주는 객체에는 구문 오류들도 포함되어 있다.

어셈블리의 산출

  • 출력 어셈블리르 생성하는 것은 간단하다. 컴파일 객체에 대해 Emit 메서드를 호출하면 된다.
EmitResult result = compilation.Emit(@"c:\temp\test.exe");
Console.WriteLine(result.Success);
  • 만일 이 메서드가 돌려준 EmitResult의 Success 속성이 false이면 산출이 실패한 것이다. 단, 만일 파일 입출력 오류 때문에 산출에 실패하면 Emit는 EmitResult를 돌려주는 대신 예외를 던진다.
  • Emit 메서드 호출시 .pdb 파일(디버그 정보가 저장됨)의 경로나 XML 문서화 파일의 경로를 지정할 수도 있다.

의미 모형 질의

  • 컴파일 객체에 대해 GetSemanticModel을 호출하면 지정된 구문 트리에 대한 의미 모형이 반환된다.
var tree = CSharpSyntaxTree.ParseText(@"class Program
{
  static void Main() => System.Console.WriteLIne(123);
}");

var compilation = CSharpCompilation.Create("test")
  .AddReference(MetadataReference.CreateFromFile(typeof(int).Assembly.Location))
  .AddSyntaxTrees(tree);

SemanticModel model = compilation.GetSemanticModel(tree);
  • GetSemancticModel 호출시 특정 구문 트리를 지정해야 하느 이유는, 하나의 컴파일 객체에 여러 개의 구문 트리가 있을 수 있기 때문이다.
  • 의미 모형이라는 것이 구문 트리와 비슷하되 속성들과 메서드들이 더 많고 그 구조가 좀 더 자세한 어떤 것이라고 추측하는 독자도 있을 것이다. 그러나 실제로는 그렇지 않다.
    • 의미 모형을 서술하는 DOM은 없다. 대신 구문 트리의 특정 위치나 노드에 관한 의미론적 정보를 돌려주는 일단의 메서드들이 있을 뿐이다.
  • 다른 말로 하면 구문 트리를 ‘운행’ 하듯이 의미 모형을 탐색할 수는 없다. 의미 모형을 사용하는 것은 스무고개 놀이와 비슷하다. 어려운 점은 어떤 질문을 던지느냐이다. 의미 모형과 관련된 메서드와 확장 메서드는 50개가 넘는다.
    • 이번 절에서는 그중 가장 흔히 쓰이는 몇 개를 소개한다. 특히 의미 모형 활용의 원리를 보여주는 메서드들을 주로 살펴보겠다.
  • 이전 예제를 이어서 다음은 식별자 ‘WriteLine”에 관한 기호 정보를 얻는 예이다.
var writeLineNode = tree.GetRoot().DescendantTokens().Single(t => t.Text == "WriteLIne").Parent;
SymbolInfo symbolInfo = model.GetSymbolInfo(writeLineNode);
Console.WriteLine(symbolInfo.Symbol);  // System.Console.WriteLine(int)
  • SymbolInfo 클래스는 기호를 대표하는 클래스이다.

기호

  • “System”이나 “Console”, “WriteLine” 같은 이름을 식별자(identifier)라고 부른다. 구문 트리에서 식별자는 IdentifierNameSyntax 형식의 노드로 존재한다. 식별자 그 자체로는 별 의미가 없다.
    • 구문 파서는 단지 식별자를 언어의 다른 키워드들과 구분할 뿐, 식별자를 ‘이해하려’ 하지 않는다.
  • 의미 모형은 식별자에 형식 정보(바인딩 단계에서 얻은)를 부여해서 기호(symbol)를 만들어 내는 능력을 가지고 있다.
  • 모든 종류의 기호는 ISymbol 인터페이스를 구현하는 특정한 기호 클래스로 대표된다. 그런데 각 기호 종류마다 좀 더 구체적인 인터페이스가 존재한다.
    • 예컨대 다음은 지금 예제의 ‘System’, ‘Console’, ‘WriteLine’에 해당하는 기호들을 대표하는 형식들이다.
"System" INamespaceSymbol
"Console" INamedTypeSymbol
"WriteLine" IMethodSymbol
  • 그리고 이러한 기호 형식 중 일부는 System.Reflection 이름공간의 형식들에 대응된다.
    • 예컨대 IMethodSymbol은 System.Reflection의 MethodInfo와 대응된다. 그러나 INamespaceSymbol 처럼 대응되는 반영 정보 형식이 없는 기호 형식들도 있다.
    • 이는 Roslyn의 형식 체계가 기본적으로 컴파일러를 위한 것이지만 반영 기능의 형식 체계는 CLR(소스 코드가 녹아 없어진 이후에 작동하는)을 위한 것이기 때문이다.
  • 그런 차이가 있긴 하지만 ISymbol 구현 형식들의 사용법은 19장에서 설명한 반영 API 사용법과 여러모로 비슷하다. 앞의 예제를 이어서 다음은 “WriteLine”에 관한 정보를 얻는 예이다.
ISymbol symbol = model.GetSymbolInfo(writeLineNode).Symbol;

Console.WriteLine(symbol.Name);  // WriteLine
Console.WriteLine(symbol.Kind);  // Method
Console.WriteLine(symbol.IsStatic);  // True
Console.WriteLine(symbol.ContainingType.Name);  // Console

var method = (IMethodSymbol) symbol;
Console.WriteLine(method.ReturnType.ToString());  // void
  • 마지막 행의 출력은 반영 API와의 미묘한 차이점을 보여준다. 출력의 “void”가 모두 소문자임을 주목하기 바란다. 이는 C#에서 쓰이는 이름이다(반면 반영 API는 언어 중립적이다).
    • 마찬가지로 System.Int32에 대한 INamedTypeSymbol 객체에 대해 ToString을 호출하면 “int”가 반환된다.
    • 다음도 반영 API로는 못하는 일의 예이다.
Console.WriteLine(symbol.Language);  // C#
  • 의미 모형에서는 또한 주어진 기호의 출처를 알아낼 수 있다.
var location = symbol.Locations.First();
Console.WriteLine(location.Kind);  // MetadataFile
Console.WriteLine(location.MetadataModule == compilation.References.Single()) // True
  • 기호가 우리 자신의 소스 코드(즉, 하나의 구문 트리)에 정의되어 있던 것이면, Location 객체의 SourceTree 속성은 그 구문 트리를 돌려주고 SourceSpan은 트리 안에서의 식별자의 위치를 돌려준다.
Console.WriteLine(location.SourceTree == null);  // True
Console.WriteLine(location.SourceSpan);  // [0..0)
  • 부분 형식은 정의가 여러 개일 수 있다. 그런 경우에는 Location이 여러 개이다.
  • 다음 질의는 WriteLine의 모든 중복적재 버전을 돌려준다.
symbol.ContainingType.GetMembers("WriteLine").OfType<IMethodSymbol>()
  • 또한 기호 객체에 대해 ToDisplayParts를 호출할 수도 있다. 그러면 전체 이름을 구성하는 ‘부품’들의 컬렉션이 반환된다. 지금 예제에서 System.Console.WriteLine(int)라는 이름은 기호 네 개에 문장 부호들이 섞인 형태이다.

SymbolInfo 클래스

  • 편집기의 코드 완성 기능을 구현한다면, 불완전하거나 부정확한 코드의 기호들을 얻어야 한다. 예컨대 다음과 같은 미완성 코드를 생각해 보자.
System.Console.WriteLine(
  • WriteLine 메서드는 중복적재되어 있으므로, 지금으로서는 WriteLine을 구체적인 하나의 ISymbol에 대응시킬 수 없다. 대신 여러 ‘후보’들을 사용자에게 제시해야 한다.
    • 이때 유용한 것이 의미 모형의 GetSymbolInfo 메서드이다. 이 메서드가 돌려주는 ISymbolInfo 구조체에는 다음과 같은 속성들이 있다.
ISymbol Symbol
ImuutableArray<ISymbol> CadidateSymbols
CadidateReason CadidateReason
  • 오류나 중의성이 있다면 Symbol 속성은 널을 돌려주며, CandidateSymbols 속성은 현재 상황과 부합하는 중복적재 버전들을 담은 컬렉션을 돌려준다. CadidateReason 속성은 무엇이 잘못되었는지를 말해주는 열거형 값을 돌려준다.
  • 코드의 특정 구간에 대한 오류와 경고 정보를 얻으려면 의미 모형에 대해 GetDiagnostics를 호출할 때 TextSpan 객체를 지정하면 된다. 인수 없이 GetDiagnostics를 호출하는 것은 CSharpCompilation 객체에 대해 GetDiagnostics를 호출하는 것과 같다.

기호의 접근성

  • ISymbol에는 DeclaredAccessibility 라는 속성이 있다. 이 속성은 주어진 기호의 접근성(public, protected, internal 등등)을 나타낸다.
    • 그런데 이 속성만으로는 소스 코드의 특정 위치에서 그 기호에 접근할 수 있는지를 확실하게 판단할 수 없다.
    • 예컨대 지역 변수는 선언된 범위 안에만 유효하며, 보호된 클래스 멤버는 해당 클래스 또는 그것을 상속한 형식의 코드에서만 접근할 수 있다.
    • 다행히 SemanticModel의 IsAccessible 메서드를 이용하면 특정 기호의 접근 가능 여부를 쉽게 파악할 수 있다.
bool canAccess = model.IsAccessible(42, someSymbol);
  • 만일 소스 코드의 오프셋 42에서 someSymbol에 접근할 수 있으면 이 호출은 true를 돌려준다.

선언된 기호

  • 형식 선언이나 멤버 선언에 대해 GetSymbolInfo를 호출하면 널이 반환된다. 예컨대 다음의 Main 메서드에 대한 기호를 얻는다고 하자.
var mainMethod = tree.GetRoot().DescendantTokens().Single(t => t.Text == "Main").Parent;

SymbolInfo symbolInfo = model.GetSymbolInfo(mainMethod);
Console.WriteLine(symbolInfo.Symbol == null);  // True
Console.WriteLine(symbolInfo.CandidateSymbols.Length);  // 0
  • 이 규칙은 형식/멤버 선언에만 적용되는 것이 아니라, 기존 기호를 소비하는 것이 아니라 새 기호를 도입하는 모든 종류의 노드에 적용된다.
  • 선언 노드에 대한 기호를 얻으려면 GetSymbolInfo 대신 GetDeclaredSymbol 메서드를 호출해야 한다.
ISymbol symbol = model.GetDeclaredSymbol(mainMethod);
  • 경우에 따라 여러 후보를 제시하기도 하는 GetSymbolInfo와 달리 GetDeclaredSymbol은 주어진 요청에 정확히 부합하는 노드의 기호 하나만 돌려주며, 그런 노드가 없으면 널을 돌려준다.
  • 또 다른 예로 Main 메서드가 다음과 같다고 ㅎ자ㅏ.
static void Main()
{
  int xyz = 123;
}
  • xyz의 형식을 다음과 같이 알안래 수 있다.
SyntaxNode variableDecl = tree.GetRoot().DescendantTokens().Single(t => t.Text == "xyz").Parent;

var local = (ILocalSymbol) model.GetDeclaredSymbol(variableDecl);
Console.WriteLine(local.Type.ToString());  // int
Console.WriteLine(local.Type.BaseType.ToString());  // System.ValueType

TypeInfo 클래스

  • 명시적인 기호가 없는 표현식이나 리터럴의 형식 정보를 알고 싶을 때도 있다. 다음 예를 생각해 보자.
var now = System.DateTime.Now;
System.Console.WriteLine(now - now);
  • now – now의 형식을 파악하려면 의미 모형에 대해 GetTypeInfo라는 메서드를 호출해야 한다.
SyntaxNode binaryExpr = tree.GetRoot().DescendantTokens().Single(t => t.Text == "-").Parent;
TypeInfo typeInfo = model.GetTypeInfo(binaryExpr);
  • 이 메서드가 돌려주는 TypeInfo에는 Type과 ConvertedType 이라는 두 속성이 있다. 후자는 임의의 암묵적 변환이 적용된 후의 형식에 해당한다.
Console.WriteLine(typeInfo.Type);  // System.TimeSpan
Console.WriteLine(typeInfo.ConvertedType);  // object
  • Console.WriteLine의 중복적재 버전 중 object를 받는 것은 있지만 TimeSpan을 받는 것은 없으므로 now – now는 암묵적으로 object로 변환된다. typeInfo.ConvertedType이 object인 것은 바로 그 때문이다.

사용 가능한 기호 찾기

  • 의미 모형의 한 가지 강력한 기능은 소스 코드의 특정 지점이 속한 범위 안의 모든 기호를 열거하는 것이다. 예컨대 IntelliSense는 사용자가 사용 가능한 기호들을 요청했을 때 바로 이러한 기능을 이용해서 기호들의 목록을 얻는다.
  • 목록을 얻으려면 그냥 소스 코드 오프셋을 인수로 해서 LookupSymbols를 호출하면 된다.
    • 다음은 이 메서드의 사용법을 보여주는 예제이다.
var tree = CSharpSyntaxTree.ParseText(@"class Program
{
  static void Main()
  {
    int x = 123, y = 234;
  }
}");

CSharpCompilation compilation = CSharpCompilation.Create("test")
  .AddReferences(MetadataReference.CreateFromFile(typeof(int).Assembly.Location))
  .AddSyntaxTrees(tree);

SemanticModel model = compilation.GetSemanticModel(tree);

// 소크 코드의 제 6행 시작 지점(닫는 중괄호 이전)에서 사용할 수 있는 기호들을 찾는다.
int index = tree.GetText().Lines[5].Start;

foreach (ISymbol symbol in model.LookupSymbols(index))
  Console.WriteLIne(symbol.ToString());
  • 결과는 다음과 같다.
y
x
Program.Main()
object.ToString()
object.Equals(object)
object.Equals(object, object)
object.ReferenceEquals(object, object);
object.GetHashCode()
object.GetType()
object.~Object()
object.MemberwiseClone()
Program
Microsoft
System
Windows
  • System 이름공간을 도입했다면 그 이름공간에 있는 형식들의 기호 수백 개도 출력되었을 것이다.

예제: 기호 이름 바꾸기

  • 지금까지 살펴본 기능들의 활용법을 보여주기 위해, 기호 이름을 바꾸는 메서드를 작성해 보자. 이 메서드는 대부분의 용법에 대해 안정적으로 작동한다. 특히
    • 형식과 멤버뿐만 아니라, 지역변수, 구간(range), 루프 변수에 대항하는 기호의 이름도 변경할 수 있다.
    • 기호가 사용된 노드뿐만 아니라 기호가 선언된 노드로도 기호를 지정할 수 있다.
    • (클래스나 구조체의 경우) 정적 생성자들과 인스턴스 생성자들의 이름도 변경한다.
    • (클래스의 경우) 종료자(소멸자)의 이름도 변경한다.
  • 간결함을 위해 새 이름이 이미 쓰이고 있는 이름인지 확인하거나 이름 변경이 실패할 만한 특별한 경우에 해당하는 기호인지 확인하는 등의 몇 가지 점검은 생략했다.
    • 그리고 다음 서명에서 보듯이, 이 메서드는 하나의 구문 트리만 고려한다.
public SyntaxTree RenameSymbol(SemanticModel model, SyntaxToken token, string newName)
  • 이 메서드를 구현하는 자명한 방법 하나는 CSharpSyntaxRewriter의 파생 클래스를 만들어서 사용하는 것이지만, 그보다 더 우아하고 유연한 접근방식이 있다. 바로 이름을 바꿀 텍스트 구간(text span)들을 직접 찾아내는 것이다.
    • 이를 위해 그런 구간들을 찾아서 돌려주는 다음과 같은 저수준 메서드를 작성하기로 하자.
public IEnumerable<TextSpan> GetRenameSpans(SemanticModel model, SyntaxToken token)
  • 예컨대 소스 코드 편집기를 만드는 경우, 편집기가 이 GetRenameSpan 메서드를 직접 호출해서 소스코드에서 변경된 부분만(취소(Undo) 트랙잭션 범위 안에서) 적용한다면 편집기 상태가 소실되어서 소스 코드 전체를 다시 파생해야 하는 사태를 피할 수 있다.
  • 주된 작업을 GetRenameSpans가 수행하므로 RenameSymbol은 간단히 구현할 수 있다.
    • 다음 예에서 보듯이 RenameSymbol은 GetRenameSpans가 돌려준 일련의 텍스트 변경들을 SourceText의 WithChanges 메서드를 이용해서 적용하면 된다.
public SyntaxTree RenameSymbol (SemanticModel model, SyntaxToken token, string newName)
{
  IEnumerable<TextSpan> renameSpans = GetRenameSpans(model, token);

  SourceText newSourceText = model.SyntaxTree.GetText().WithChanges(
    renameSpans.Select(span => new TextChange(span, newName)).OrderBy(tc => tc));

  return model.SyntaxTree.WithChangedText(newSourceText);
}
  • 만일 변경들의 순서가 맞지 않으면 WithChanges는 예외를 던진다. OrderBy로 변경들을 정렬하는 것은 그 때문이다.
  • 이제 GetRenameSpans를 구현해 보자. 우선 할 일은 이름을 바꾸고자 하는 토큰에 해당하는 기호를 찾는 것이다.
    • 그 토큰은 선언의 일부일 수도 있고 실제로 사용되는 코드의 일부일 수도 있다.
    • 일단은 GetSymbolInfo를 호출해서 찾고, 만일 그 결과가 널이면 GetDeclaredSymbol을 호출한다.
public IEnumerable<TextSpan> GetRenameSpans(SemanticModel model, SyntaxToken token)
{
  var node = token.Parent;

  ISymbol symbol = model.GetSymbolInfo(node).Symbol ?? model.GetDeclaredSymbol(node);

  if (symbol == null) return null;  // 이름을 바꿀 기호가 없음.
  • 다음으로는 기호의 정의를 찾아야 한다. 기호가 정의된 장소는 기호의 Locations 속서응로 알아낼 수 있다. (부분 클래스와 부분 메서드 때문에 여러 장소를 고려하긴 하지만, 사실 부분 정의를 제대로 처리하려면 여러 개의 구문 트리를 검색하도록 이 예제를 확장해야 할 것이다.)
var definitions = 
  from location in symbol.Locations
  where location.SourceTree == node.SyntaxTree
  select location.SourceSpan;
  • 다음으로는 기호가 쓰인 곳들을 찾아야 한다. 이를 위해, 우선 기호의 이름과 일치하는 이름을 가진 후손 토큰들을 찾는다. 이렇게 하면 해당 기호와 무관한 토큰들 대부분을 빠르게 배제할 수 있다.
    • 그런 다음에는 각 토큰의 부모 노드에 대해 GetSymbolInfo를 호출해서, 해당 기호가 우리가 이름을 바꾸고자 하는 기호인지 점검한다.
var usages = 
  from t in model.SyntaxTree.GetRoot().DescendantToken()
  where t.Text == symbol.Name
  let s = model.GetSymbolInfo (t.Parent).Symbol
  where s == symbol
  select t.Span;
  • Roslyn 라이브러리에서 기호 정보 조회 등 바인딩과 관련된 연산들은 그냥 텍스트나 구문 트리를 조사하는 연산들보다 느린 경향이 있다.
    • 이는 바인딩을 고려하려면 어셈블리에서 형식들을 검색하고, 형식 추론 규칙들을 적용하고, 확장 메서드들을 점검하는 등의 고비용 연산이 필요하기 때문이다.
  • 기호가 이름이 붙은 형식 이외의 토큰(지역 변수, 범위 변수 등)에 대한 기호이면 추가적인 처리는 필요없다. 기호가 정의된 곳들과 기호가 쓰인 곳들을 합쳐서 반환하면 그만이다.
if (symbol.Kind != SymbolKind.NamedType)
  return definitions.Concat(usages);
  • 기호가 이름이 붙은 형식(클래스 등)이면 생성자들과 소멸자들이 있는지 확인해서 그것들의 이름도 바꾸어야 한다.
    • 이를 위해 우선 후손 노드들을 열거하면서 바꿀 이름과 같은 이름의 형식 선언들을 찾고, 해당 노드의 선언된 기호를 얻어서 그 기호의 이름이 우리가 바꾸려는 기호의 이름과 같은지 점검한다.
    • 만일 같다면 생성자 메서드들과 소멸자 메서드들을 찾아서 해당 텍스트 구간들을 다른 텍스트 구간들(기호가 정의된 곳들과 기호가 쓰인 곳들)에 합친 후 반환한다.
var structors =
  from type in model.SyntaxTree.GetRoot().DescendantNodes().OfType<TypeDeclarationSyntax>()
  where type.Identifier.Text == symbol.Name
  let declaredSymbol == model.GetDeclaredSymbol(type)
  where declaredSymbol == symbol
  from method in type.Members
  let constructor = method as ConstructorDeclarationSyntax
  let destructor = method as DestructorDeclarationSyntax
  where construtor != null || destructor != null
  let identifier = constructor?.Identifier ?? destructor.Identifier
  select identifier.Span;

return definitions.Concat(usages).Concat(structors);
}
  • 다음은 이상의 두 메서드의 사용법을 보여주는 예제이다. 두 메서드의 완전한 정의도 포함되어 있다.
void Demo()
{
  var tree = CSharpSyntaxTree.ParseText(@"class Program
  {
    static Program() {}
    public Program() {}

    staic void Main()
    {
      Program p = new Program();
      p.Foo();
    }

    static void Foo() => Bar();
    static void Bar() => Foo();
  }");

  var compilation = CSharpCompilation.Create("test")
    .AddReferences(MetadataReference.CreateFromFile(typeof(int).Assembly.Location))
    .AddSyntaxTrees(tree);

  var model = compilation.GetSemanticModel(tree);
  var tokens = tree.GetRoot().DescendantTokens();

  // Program 클래스의 이름을 Program2로 바꾼다.
  SyntaxToken foo = tokens.Last(t => t.Text == "Foo");
  Console.WriteLine(RenameSymbol(model, foo, "Foo2").ToString());

  // 지역 변수 p의 이름을 p2로 바꾼다
  SyntaxToken p = tokens.Last(t => t.Text == "p");
  Console.WriteLine(RenameSymbol(model, p, "p2").ToString());
}

public SyntaxTree RenameSymbol(SemanticModel model, SyntaxToken token, string newName)
{
  IEnumerable renameSpans = GetRenameSpans(model, token).OrderBy(s => s);
  SourceText newSourceText = model.SyntaxTree.GetText().WithChanges(
    renameSpans.Select(s => new TextChange(s, newName)));

  return model.SyntaxTree.WithChangedText(newSourceText);
}

public IEnumerable GetRenameSpans(SemanticModel model, SyntaxToken token)
{
  var node = token.Parent;

  ISymbol symbol = model.GetSymbolInfo(node).Symbol ?? medel.GetDeclaredSymbol(node);

  if (symbol == null) return null;  // 이름을 바꿀 기호가 없음

  var definitions =
    from location in symbol.Locations
    where location.SourceTree == node.SyntaxTree
    select location.SourceSpan;

  var usage =
    from t in model.SyntaxTree.GetRoot().DescendantTokens()
    where t.Text == symbol.Name
    let st = model.GetSymbolInfo(t.Parent).Symbol
    where s == symbol
    select t.Span;

  if (symbol.Kind != SymbolKind.NamedType)
    return definitions.Concat(usage);

  var structors =
    from type in model.SyntaxTree.GetRoot().DescendantNodes().OfType()
    where type.Identifier.Text == symbol.Name
    let declaredSymbol == model.GetDeclaredSymbol(type)
    where declaredSymbol == symbol
    from method in type.Members
    let constructor = method as ConstructorDeclarationSyntax
    let destructor = method as DestructorDeclarationSyntax
    where construtor != null || destructor != null
    let identifier = constructor?.Identifier ?? destructor.Identifier
    select identifier.Span;

  return definitions.Concat(usages).Concat(structors);
}

 

[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.