[제목] 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)
본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.