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