Roslyn - C# 컴파일러 API 에 대하여 (3)

[제목] Roslyn - C# 컴파일러 API 에 대하여 (3)

이번 아티클에서는 Senmatic Analysis와 관련된 API 에 대해 좀 더 자세히 알아보고자 한다. 지난 아티클에서 CSharpCompilation 객체를 생성하는 부분을 설명했었는데, 이 CSharpCompilation 객체가 생성된 후에 해당 객체의 GlobalNamespace 속성을 참조하면, 소스코드로부터 얻어진 심벌들을 계층적으로 검색할 수 있다. GlobalNamespace 속성은 INamespaceSymbol 인터페이스를 갖고, 이로부터 GetMembers() 호출하면 하위 심벌 노드들을 얻어낼 수 있다. 즉, GlobalNamespace은 심벌 트리의 루트에 해당되므로 계속 하위노드를 검색하여 필요한 심벌을 찾아낼 수 있다.


SyntaxTree tree = CSharpSyntaxTree.ParseText(@"
using System;
namespace NS1 { 
    class Class1 { 
        private int _flag;
        public int ID { get; set; }
        public string Name { get; set; }
        public void Run(int id) {
           int a = 1;
           a = a + id * 10;
           Console.WriteLine(a);
        }
    } 

    class Class2 {} 
}");

// Compilation            
var compilation = CSharpCompilation.Create("Hello")
   .AddReferences(new MetadataFileReference(typeof(object).Assembly.Location))
   .AddSyntaxTrees(tree);

// GlobalNamespace 심벌
INamespaceSymbol globalNamespace = compilation.GlobalNamespace;
foreach (ISymbol symbol in globalNamespace.GetMembers())
{ 
    if (symbol.Name == "NS1") 
    {
        // namespace 심벌
        var nsSym = (INamespaceSymbol)symbol;

        // class 심벌
        foreach (var classSymbol in nsSym.GetMembers())
        {
            Console.WriteLine(classSymbol.ToDisplayString());
        }
    }
}

위의 예제는 GlobalNamespace 바로 밑에 있는 Namespace NS1 심벌을 찾아내고, 다시 NS1의 자식 노드인 클래스 심벌(Class1, Class2)들을 출력하는 코드이다.

위의 심벌 검색 방식 즉 Compilation.GlobalNamespace 속성의 하위 노드를 검색하는 방식과는 다른 방식으로, 특정 Syntax Tree 노드로부터 직접 심벌을 찾아내는 방식이 있다. Syntax Tree의 노드로부터 그 노드가 갖는 심벌이나 의미를 파악하기 위해 SemanticModel 클래스 객체를 사용하는데, 이는 CSharpCompilation 클래스의 GetSemanticModel() 메서드를 사용하여 얻을 수 있다. CSharpCompilation 에는 1개 이상의 Syntax Tree를 가질 수 있으므로, GetSemanticModel() 의 파라미터로 특정 SyntaxTree 객체를 지정하고 이 트리에 대한 SemanticModel 객체를 가져와 Semantic Analysis를 실행하게 된다.

예를 들어, 아래 코드와 같이 Syntaxt Tree의 루트 노트로부터 첫 MethodDeclarationSyntax 노드를 검색한 후, GetDeclaredSymbol() 메서드를 사용하여 이 노드의 메서드 심벌을 직접 얻어 낼 수 있다.

// Syntax tree에 대한 Semantic Model 객체 
SemanticModel model = compilation.GetSemanticModel(tree);
// 메서드 트리노드 찾기
var methodDecl = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First();
// 메서드 심벌 리턴
ISymbol methodSym = model.GetDeclaredSymbol(methodDecl);
Console.WriteLine(methodSym.ToDisplayString());

또 다른 예제로서 아래 코드는 첫번째 클래스 노드 즉 Class1 노드의 심벌을 구한 후, 이 클래스의 멤버들(메서드, 필드 이벤트 등)에 대한 심벌을 출력하는 예이다.

var classNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>().First();
INamedTypeSymbol clsSym = model.GetDeclaredSymbol(classNode);  
foreach (var s in clsSym.GetMembers())
{                
    if (s is IMethodSymbol)
    {
        // 메서드 심벌 출력                
        Console.WriteLine("Method: {0}", s);                    
    }
    else if (s is IFieldSymbol)
    {
        // 필드 심벌 출력                
        Console.WriteLine("Field: {0}", s);
    }
    else
    {
        // skip
    }
}

C# 코드에서 특정 메서드를 호출하거나 람다식과 같은 호출을 진행할 때, 이는 InvocationExpressionSyntax 노드로 표현된다. 아래 예제는 이 InvocationExpressionSyntax 노드로부터 Symbol 정보를 얻어와서 이 메서드가 어떤 DLL의, 어떤 네임스페이스 밑의, 어떤 클래스에 있는 메서드 인지를 체크할 수 있음을 보여준다.

// Run() 안의 Console.WriteLine() 호출
InvocationExpressionSyntax invokeExprNode = methodDecl.DescendantNodes().OfType<InvocationExpressionSyntax>().First();
string exprText = invokeExprNode.ToString();
Console.WriteLine("Raw Text : {0}", exprText);

SymbolInfo si = model.GetSymbolInfo(invokeExprNode);
ISymbol symb = si.Symbol;
if (symb.ContainingAssembly.Name == "mscorlib" && symb.ContainingNamespace.Name == "System" &&
    symb.ContainingType.Name == "Console" && symb.Name == "WriteLine")
{
    Console.WriteLine("System.Console 클래스의 WriteLine 메서드임");
}

Semantic Analysis 는 또한 표현식에 대하여 해당 표현식의 결과가 어떤 타입으로 리턴되는지를 알 수 있는 기능을 제공 한다. 예를 들어, 아래 코드는 산술식에 대한 결과로서 int 타입이 리턴된다는 것을 의미 분석을 통해 알려 준다.

// 표현식 : a = a + id * 10
var exprDecl = methodDecl.DescendantNodes().OfType<BinaryExpressionSyntax>().First();            
TypeInfo typeInfo = model.GetTypeInfo(exprDecl);            
Console.WriteLine(typeInfo.Type);  // int

Semantic Analysis 는 여러 용도로 활용될 수 있다. 예를 들어, VS의 Go to definition 기능, Refactoring, Intelligent Rename, Senamtic 에러/경고 메서지 등등에 유용하게 활용될 수 있다.

Roslyn - C# 컴파일러 API (1)
Roslyn - C# 컴파일러 API (2)
Roslyn - C# 컴파일러 API (3)



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.