이 자습서에서는 구문 API를 탐색합니다. 구문 API는 C# 또는 Visual Basic 프로그램을 설명하는 데이터 구조에 대한 액세스를 제공합니다. 이러한 데이터 구조에는 모든 크기의 프로그램을 완전히 나타낼 수 있는 충분한 세부 정보가 있습니다. 이러한 구조는 올바르게 컴파일하고 실행하는 전체 프로그램을 설명할 수 있습니다. 편집기에서 작성할 때 불완전한 프로그램을 설명할 수도 있습니다.
이 풍부한 식을 사용하도록 설정하려면 구문 API를 구성하는 데이터 구조와 API가 반드시 복잡합니다. 일반적인 "Hello World" 프로그램에 대한 데이터 구조의 모양부터 시작해 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
이전 프로그램의 텍스트를 확인합니다. 친숙한 요소를 인식합니다. 전체 텍스트는 단일 소스 파일 또는 컴파일 단위를 나타냅니다. 해당 소스 파일의 처음 세 줄은 지시문을 사용합니다. 나머지 원본은 네임스페이스 선언에 포함됩니다. 네임스페이스 선언에는 자식 클래스 선언이 포함됩니다. 클래스 선언에는 하나의 메서드 선언이 포함됩니다.
구문 API는 컴파일 단위를 나타내는 루트를 사용하여 트리 구조를 만듭니다. 트리의 노드는 지시문, 네임스페이스 선언 및 프로그램의 다른 모든 요소를 나타냅니다 using . 트리 구조는 가장 낮은 수준까지 계속됩니다. "Hello World!" 문자열은 인수의 하위 항목인 문자열 리터럴 토큰입니다. 구문 API는 프로그램의 구조에 대한 액세스를 제공합니다. 특정 코드 사례를 쿼리하고, 전체 트리를 탐색하여 코드를 이해하고, 기존 트리를 수정하여 새 트리를 만들 수 있습니다.
이 간략한 설명은 구문 API를 사용하여 액세스할 수 있는 정보의 종류에 대한 개요를 제공합니다. 구문 API는 C#에서 알고 있는 친숙한 코드 구문을 설명하는 공식 API에 지나지 않습니다. 전체 기능에는 줄 바꿈, 공백 및 인덴팅을 포함하여 코드의 형식을 지정하는 방법에 대한 정보가 포함됩니다. 이 정보를 사용하면 휴먼 프로그래머 또는 컴파일러가 작성하고 읽은 코드를 완전히 나타낼 수 있습니다. 이 구조를 사용하면 매우 의미 있는 수준에서 소스 코드와 상호 작용할 수 있습니다. 더 이상 텍스트 문자열이 아니라 C# 프로그램의 구조를 나타내는 데이터입니다.
시작하려면 .NET 컴파일러 플랫폼 SDK를 설치해야 합니다.
설치 방법 - Visual Studio 설치 프로그램
Visual Studio 설치관리자에서 .NET 컴파일러 플랫폼 SDK를 찾는 방법에는 두 가지가 있습니다.
Visual Studio 설치 관리자를 사용하여 설치 - 워크로드 보기
.NET 컴파일러 플랫폼 SDK는 Visual Studio 확장 개발 워크로드의 일부로 자동으로 선택되지 않습니다. 선택적 구성 요소로 선택해야 합니다.
- Visual Studio 설치 관리자 실행
- 수정을 선택합니다.
- Visual Studio 확장 개발 워크로드를 확인합니다.
- 요약 트리에서 Visual Studio 확장 개발 노드를 엽니다.
- .NET 컴파일러 플랫폼 SDK 상자가 선택되어 있는지 확인합니다.
- 수정을 선택합니다.
필요에 따라 DGML 편집 기에서 시각화 도우미에 그래프를 표시할 수도 있습니다.
- 요약 트리에서 개별 구성 요소 노드를 엽니다.
- DGML 편집기 확인란을 선택하세요.
Visual Studio 설치 관리자를 사용하여 설치 - 개별 구성 요소 탭
- Visual Studio 설치 관리자 실행
- 수정을 선택합니다.
- 개별 구성 요소 탭 선택
- .NET 컴파일러 플랫폼 SDK에 대한 확인란을 선택합니다. 컴파일러, 빌드 도구 및 런타임 섹션 아래에 맨 위에 있습니다.
- 수정을 선택합니다.
필요에 따라 DGML 편집 기에서 시각화 도우미에 그래프를 표시할 수도 있습니다.
- DGML 편집기의 확인란을 체크합니다. 코드 도구 섹션에서 찾을 수 있습니다.
구문 트리 이해
C# 코드의 구조를 분석하는 데 구문 API를 사용합니다. 구문 API는 구문 트리를 분석하고 생성하기 위한 파서, 구문 트리 및 유틸리티를 노출합니다. 특정 구문 요소에 대한 코드를 검색하거나 프로그램의 코드를 읽는 방법입니다.
구문 트리는 C# 및 Visual Basic 컴파일러에서 C# 및 Visual Basic 프로그램을 이해하는 데 사용하는 데이터 구조입니다. 구문 트리는 프로젝트가 빌드되거나 개발자가 F5에 도달할 때 실행되는 동일한 파서에 의해 생성됩니다. 구문 트리는 언어에 대한 완전한 충실도를 갖습니다. 코드 파일의 모든 정보 비트는 트리에 표시됩니다. 구문 트리를 텍스트에 쓰면 구문 분석된 정확한 원본 텍스트가 재현됩니다. 구문 트리도 변경할 수 없습니다. 구문 트리를 만든 후에는 변경할 수 없습니다. 트리의 소비자는 데이터가 변경되지 않음을 알면 잠금 또는 기타 동시성 측정값 없이 여러 스레드에서 트리를 분석할 수 있습니다. API를 사용하여 기존 트리를 수정한 결과인 새 트리를 만들 수 있습니다.
구문 트리의 네 가지 기본 구성 요소는 다음과 같습니다.
- Microsoft.CodeAnalysis.SyntaxTree 전체 구문 분석 트리를 나타내는 인스턴스인 클래스입니다. SyntaxTree 는 언어별 파생체가 있는 추상 클래스입니다. (또는Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree) 클래스의 Microsoft.CodeAnalysis.VisualBasic.VisualBasicSyntaxTree 구문 분석 메서드를 사용하여 C#(또는 Visual Basic)에서 텍스트를 구문 분석합니다.
- Microsoft.CodeAnalysis.SyntaxNode 클래스의 인스턴스는 선언, 문, 절 및 식과 같은 구문 구조를 나타냅니다.
- Microsoft.CodeAnalysis.SyntaxToken 개별 키워드, 식별자, 연산자 또는 문장 부호를 나타내는 구조체입니다.
- Microsoft.CodeAnalysis.SyntaxTrivia 마지막으로 토큰, 전처리 지시문 및 주석 사이의 공백과 같은 구문적으로 중요하지 않은 정보 비트를 나타내는 구조체입니다.
퀴즈, 토큰 및 노드는 Visual Basic 또는 C# 코드 조각의 모든 항목을 완전히 나타내는 트리를 형성하기 위해 계층적으로 구성됩니다. 구문 시각화 도우미 창을 사용하여 이 구조를 볼 수 있습니다. Visual Studio에서다른 Windows>구문 시각화 도우미> 선택합니다. 예를 들어 구문 시각화 도우미 를 사용하여 검사된 이전 C# 원본 파일은 다음 그림과 같습니다.
SyntaxNode: Blue | SyntaxToken: 녹색 | SyntaxTrivia: Red 
이 트리 구조를 탐색하면 코드 파일에서 문, 식, 토큰 또는 약간의 공백을 찾을 수 있습니다.
구문 API를 사용하여 코드 파일에서 아무것도 찾을 수 있지만 대부분의 시나리오에는 코드의 작은 조각을 검사하거나 특정 문 또는 조각을 검색하는 작업이 포함됩니다. 다음 두 예제에서는 코드 구조를 찾아보거나 단일 문을 검색하는 일반적인 용도를 보여 줍니다.
트리 탐색
두 가지 방법으로 구문 트리에서 노드를 검사할 수 있습니다. 트리를 트래버스하여 각 노드를 검사하거나 특정 요소 또는 노드를 쿼리할 수 있습니다.
수동 순회
GitHub 리포지토리에서 이 샘플에 대한 완성된 코드를 볼 수 있습니다.
비고
구문 트리 형식은 상속을 사용하여 프로그램의 여러 위치에서 유효한 다양한 구문 요소를 설명합니다. 이러한 API를 사용하면 속성 또는 컬렉션 멤버를 특정 파생 형식으로 캐스팅하는 경우가 많습니다. 다음 예제에서 할당과 캐스트는 명시적으로 형식화된 변수를 사용하는 별도의 문입니다. 코드를 읽어 API의 반환 형식과 반환된 개체의 런타임 형식을 확인할 수 있습니다. 실제로 암시적으로 형식화된 변수를 사용하고 API 이름을 사용하여 검사되는 개체의 형식을 설명하는 것이 더 일반적입니다.
새 C# Stand-Alone 코드 분석 도구 프로젝트를 만듭니다.
- Visual Studio에서 새 프로젝트 파일을> 선택하여새>프로젝트 대화 상자를 표시합니다.
- Visual C#>확장성 아래에서 Stand-Alone 코드 분석 도구를 선택합니다.
- 프로젝트 이름을 "SyntaxTreeManualTraversal"로 지정하고 확인을 클릭합니다.
앞에서 보여 준 기본 "Hello World!" 프로그램을 분석합니다.
Hello World 프로그램의 텍스트를 클래스의 상수로 추가합니다 Program .
const string programText =
@"using System;
using System.Collections;
using System.Linq;
using System.Text;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";
다음으로, 다음 코드를 추가하여 상수의 코드 텍스트에 대한 구문 트리 를 programText 작성합니다. 메서드에 다음 줄을 추가합니다 Main .
SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
이 두 줄은 트리를 만들고 해당 트리의 루트 노드를 검색합니다. 이제 트리의 노드를 검사할 수 있습니다. 트리에서 루트 노드의 속성 중 일부를 표시하려면 다음 줄을 메서드에 추가합니다 Main .
WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using directives. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
WriteLine($"\t{element.Name}");
애플리케이션을 실행하여 코드가 이 트리의 루트 노드에 대해 검색한 내용을 확인합니다.
일반적으로 트리를 트래버스하여 코드에 대해 알아봅니다. 이 예제에서는 API를 탐색하기 위해 알고 있는 코드를 분석합니다. 다음 코드를 추가하여 노드의 첫 번째 멤버를 검사합니다 root .
MemberDeclarationSyntax firstMember = root.Members[0];
WriteLine($"The first member is a {firstMember.Kind()}.");
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;
해당 멤버는 Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax입니다. 선언 범위의 namespace HelloWorld 모든 항목을 나타냅니다. 다음 코드를 추가하여 HelloWorld 네임스페이스 내에 선언된 노드를 검사합니다.
WriteLine($"There are {helloWorldDeclaration.Members.Count} members declared in this namespace.");
WriteLine($"The first member is a {helloWorldDeclaration.Members[0].Kind()}.");
프로그램을 실행하여 배운 내용을 확인합니다.
이제 선언이 a라는 Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax것을 알게 되었으므로 해당 형식의 새 변수를 선언하여 클래스 선언을 검사합니다. 이 클래스에는 하나의 멤버 Main 인 메서드만 포함됩니다. 다음 코드를 추가하여 Main 메서드를 찾아서, 이를 Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax로 캐스팅합니다.
var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
WriteLine($"There are {programDeclaration.Members.Count} members declared in the {programDeclaration.Identifier} class.");
WriteLine($"The first member is a {programDeclaration.Members[0].Kind()}.");
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];
메서드 선언 노드에는 메서드에 대한 모든 구문 정보가 포함됩니다. 메서드의 Main 반환 형식, 인수의 수 및 형식 및 메서드의 본문 텍스트를 표시해 보겠습니다. 다음 코드를 추가합니다.
WriteLine($"The return type of the {mainDeclaration.Identifier} method is {mainDeclaration.ReturnType}.");
WriteLine($"The method has {mainDeclaration.ParameterList.Parameters.Count} parameters.");
foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
WriteLine($"The type of the {item.Identifier} parameter is {item.Type}.");
WriteLine($"The body text of the {mainDeclaration.Identifier} method follows:");
WriteLine(mainDeclaration.Body?.ToFullString());
var argsParameter = mainDeclaration.ParameterList.Parameters[0];
프로그램을 실행하여 이 프로그램에 대해 검색한 모든 정보를 확인합니다.
The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using directives. They are:
System
System.Collections
System.Linq
System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
{
Console.WriteLine("Hello, World!");
}
쿼리 메서드
트리를 트래버스하는 것 외에도 에 정의된 쿼리 메서드를 사용하여 구문 트리를 탐색할 수도 있습니다 Microsoft.CodeAnalysis.SyntaxNode. 이러한 메서드는 XPath에 익숙한 모든 사용자에게 즉시 친숙해야 합니다. LINQ에서 이러한 메서드를 사용하여 트리에서 항목을 빠르게 찾을 수 있습니다. SyntaxNode는 DescendantNodes, AncestorsAndSelf, ChildNodes 등의 쿼리 메서드를 제공합니다.
이러한 쿼리 메서드를 사용하여 트리 탐색 대신 메서드에 대한 인수 Main 를 찾을 수 있습니다. 메서드의 맨 아래에 다음 코드를 추가합니다 Main .
var firstParameters = from methodDeclaration in root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
where methodDeclaration.Identifier.ValueText == "Main"
select methodDeclaration.ParameterList.Parameters.First();
var argsParameter2 = firstParameters.Single();
WriteLine(argsParameter == argsParameter2);
첫 번째 문은 LINQ 식과 메서드를 사용하여 이전 예제와 DescendantNodes 동일한 매개 변수를 찾습니다.
프로그램을 실행하면, 트리를 수동으로 탐색하는 것과 동일한 매개 변수를 LINQ 식이 찾은 것을 볼 수 있습니다.
이 샘플에서는 WriteLine 문을 사용함으로써 구문 트리가 탐색될 때 구문 트리에 대한 정보를 표시합니다. 또한 디버거에서 완성된 프로그램을 실행하여 훨씬 더 많은 것을 배울 수 있습니다. hello world 프로그램에 대해 만든 구문 트리의 일부인 속성과 메서드를 더 자세히 살펴볼 수 있습니다.
구문 분석기
종종 구문 트리에서 특정 형식의 모든 노드를 찾으려고 합니다(예: 파일의 모든 속성 선언). 클래스를 Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker 확장하고 메서드를 재정의하면 VisitPropertyDeclaration(PropertyDeclarationSyntax) 해당 구조를 미리 알지 못하고 구문 트리의 모든 속성 선언을 처리합니다. CSharpSyntaxWalker 는 노드와 각 자식을 재귀적으로 방문하는 특정 종류 CSharpSyntaxVisitor 입니다.
구문 트리를 검사하는 CSharpSyntaxWalker를 구현한 예제입니다. 네임스페이스를 System에 가져오지 않는 using 지시문을 수집합니다.
새 C# Stand-Alone 코드 분석 도구 프로젝트를 만듭니다. 이름을 "SyntaxWalker"로 지정합니다.
GitHub 리포지토리에서 이 샘플에 대한 완성된 코드를 볼 수 있습니다. GitHub의 샘플에는 이 자습서에 설명된 두 프로젝트가 모두 포함되어 있습니다.
이전 샘플에서와 같이 분석하려는 프로그램의 텍스트를 저장할 문자열 상수는 다음과 같이 정의할 수 있습니다.
const string programText =
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace TopLevel
{
using Microsoft;
using System.ComponentModel;
namespace Child1
{
using Microsoft.Win32;
using System.Runtime.InteropServices;
class Foo { }
}
namespace Child2
{
using System.CodeDom;
using Microsoft.CSharp;
class Bar { }
}
}";
이 원본 텍스트에는 파일 수준, 최상위 네임스페이스 및 중첩된 두 네임스페이스의 네 가지 위치에 분산된 지시문이 포함되어 using 있습니다. 이 예제는 CSharpSyntaxWalker 클래스를 사용하여 코드에 쿼리를 수행하는 핵심 시나리오를 강조합니다. 루트 구문 트리의 모든 노드를 방문하여 선언을 사용하여 찾는 것은 번거로울 수 있습니다. 대신 파생 클래스를 만들고, 트리에서 현재 노드가 using 지시문일 때만 호출되는 메서드를 재정의합니다. 방문자는 다른 노드 형식에서 아무 작업도 수행하지 않습니다. 이 단일 메서드는 각 using 지시문을 검사하고 네임스페이스에 없는 System 네임스페이스의 컬렉션을 빌드합니다.
CSharpSyntaxWalker을/를 구성하여 모든 using 지시문과 using 지시문만 검사합니다.
이제 프로그램 텍스트를 정의했으므로 해당 트리의 루트를 SyntaxTree 만들고 가져와야 합니다.
SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
다음으로, 새 클래스를 만듭니다. Visual Studio에서 프로젝트>새 항목 추가를 선택합니다. 새 항목 추가 대화 상자에서 파일 이름으로 UsingCollector.cs 입력합니다.
using 방문자 기능을 UsingCollector 클래스에 구현합니다. 먼저 클래스가 UsingCollector .에서 CSharpSyntaxWalker파생되도록 만듭니다.
class UsingCollector : CSharpSyntaxWalker
수집 중인 네임스페이스 노드를 저장하려면 스토리지가 필요합니다. 클래스에서 UsingCollector 공용 읽기 전용 속성을 선언합니다. 이 변수를 사용하여 찾은 노드를 UsingDirectiveSyntax 저장합니다.
public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();
기본 클래스 CSharpSyntaxWalker 는 구문 트리의 각 노드를 방문하는 논리를 구현합니다. 파생 클래스는 관심 있는 특정 노드에 대해 호출된 메서드를 재정의합니다. 이 경우 어느 using 지시문에 관심이 있습니다. 즉, VisitUsingDirective(UsingDirectiveSyntax) 메서드를 재정의해야 합니다. 이 메서드의 인수 중 하나는 개체입니다 Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax . 이는 방문자를 사용하는 데 있어 중요한 이점입니다. 이미 특정 노드 형식으로 캐스팅된 인수를 사용하여 재정의된 메서드를 호출합니다. 클래스에는 Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntaxName 가져올 네임스페이스의 이름을 저장하는 속성이 있습니다.
Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax입니다.
VisitUsingDirective(UsingDirectiveSyntax) 재정의에 다음 코드를 추가합니다.
public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
WriteLine($"\tVisitUsingDirective called with {node.Name}.");
if (node.Name.ToString() != "System" &&
!node.Name.ToString().StartsWith("System."))
{
WriteLine($"\t\tSuccess. Adding {node.Name}.");
this.Usings.Add(node);
}
}
이전 예제와 마찬가지로 이 메서드를 이해하는 데 도움이 되는 다양한 WriteLine 문을 추가했습니다. 호출될 때와 매번 전달되는 인수를 확인할 수 있습니다.
마지막으로 두 줄의 코드를 추가하여 UsingCollector를 만들고 루트 노드를 방문하여 모든 using 지시문을 수집하도록 해야 합니다. 그런 다음, 루프를 foreach 추가하여 수집기가 찾은 using 모든 지시문을 표시합니다.
var collector = new UsingCollector();
collector.Visit(root);
foreach (var directive in collector.Usings)
{
WriteLine(directive.Name);
}
프로그램을 컴파일하고 실행합니다. 다음과 같은 출력이 표시됩니다.
VisitUsingDirective called with System.
VisitUsingDirective called with System.Collections.Generic.
VisitUsingDirective called with System.Linq.
VisitUsingDirective called with System.Text.
VisitUsingDirective called with Microsoft.CodeAnalysis.
Success. Adding Microsoft.CodeAnalysis.
VisitUsingDirective called with Microsoft.CodeAnalysis.CSharp.
Success. Adding Microsoft.CodeAnalysis.CSharp.
VisitUsingDirective called with Microsoft.
Success. Adding Microsoft.
VisitUsingDirective called with System.ComponentModel.
VisitUsingDirective called with Microsoft.Win32.
Success. Adding Microsoft.Win32.
VisitUsingDirective called with System.Runtime.InteropServices.
VisitUsingDirective called with System.CodeDom.
VisitUsingDirective called with Microsoft.CSharp.
Success. Adding Microsoft.CSharp.
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.CSharp
Microsoft
Microsoft.Win32
Microsoft.CSharp
Press any key to continue . . .
축하합니다! 구문 API를 사용하여 C# 소스 코드에서 특정 종류의 지시문 및 선언을 찾습니다.
.NET