The Roslyn Project 번역판


* 이건 제 개인적인 번역본 입니다. 공식적인 번역본이 만약 나오면 지우겠습니다.

Exposing the C# and VB compiler’s code analysis

October 2011

Karen Ng, Principal Lead Program Manager, Microsoft Corporation
Matt Warren, Principal Architect, Microsoft Corporation
Peter Golde, Partner Architect, Microsoft Corporation
Anders Hejlsberg, Technical Fellow, Microsoft Corporation

Contents

1 Introduction

2 Exposing the Core APIs

2.1 Key Concepts

2.2 API Layers

2.2.1 Compiler APIs

2.2.2 Scripting APIs

2.2.3 Workspace APIs

2.2.4 Services APIs

3 Working with Syntax

3.1 Key Concepts

3.1.1 Syntax Trees

3.1.2 Syntax Nodes

3.1.3 Syntax Tokens

3.1.4 Syntax Trivia

3.1.5 Spans

3.1.6 Kinds

3.1.7 Errors

3.2 Obtaining a Syntax Tree

3.3 Manipulating Syntax Nodes

3.3.1 Creating a node

3.3.2 Modifying an existing node

3.3.3 Replacing a node

3.3.4 Creating a Syntax Tree

4 Working with Semantics

4.1 Key Concepts

4.1.1 Compilation

4.1.2 Symbols

4.1.3 Semantic Model

4.2 Obtaining a Compilation

4.3 Obtaining Symbols

4.4 Asking Semantic Questions

4.5 Working with Fault Tolerance

4.6 Using the Control and Data Flow Analysis APIs

5 Working with a Workspace

5.1 Key Concepts

5.1.1 Workspace

5.1.2 Solutions, Projects, Document

5.2 Obtaining a Workspace

5.3 Working with a Solution

5.4 Updating a Workspace

6 Working with Services

6.1 Key Concepts

6.1.1 Managed Extensibility Framework

6.2 Creating an IDE extension

7 Summary

1 Introduction

전통적으로, 컴파일러는 블랙박스였습니다. 소스코드가 한쪽으로 들어가고, 중간에서 마법이 일어나, 마지막에 어셈블리가 되는거였습니다. 컴파일러가 그 마법을 수행하는 동안, 소스코드에 대한 많은 유용한 정보들을 알아내지만, 그 정보들은 바깥에서는 볼수 없었죠. 그리고 그 정보들은 마지막에 모두 버려졌습니다.

수세기동안, 사람들은 이런 방식만으로도 충분히 만족했지만, 점차 IDE가 대세로 자리 잡아 감에 따라, intelliSense, refactoring, rename 등등 컴파일러의 저 버려지는 정보들을 원하는 기능들이 많아져 갔습니다. 로잘린은 바로 이 부분을 채워주기 위한 목적으로 만들어진 프로젝트입니다. 블랙박스를 오픈하여 여러 스마트한 툴들과 사용자들에게 컴파일러가 가지고 있는 코드에 대한 정보를 제공해 주는것이죠. 로즐린을 통해 컴파일러가 서비스가 되는것입니다.

이 로즐린 프로젝트를 통해 스마트한 코드 툴 시장에 들어오는 장벽을 낮춰, 메타 프로그램밍, code generation and transformation, 인터렉티브한 C#, VB 코드, DSL에 C#/VB 삽입등과 같은 여러 분야의 시너지 효과를 기대해 봅니다.

The Microsoft “Roslyn” CTP 은 코드 생성, 분석, 리펙토링, 스크립팅등을 위한 새로운 랭귀지 오브젝트 모델을 제시합니다. 이 문서는 로즐린 프로젝트에 대한 conceptual overview 입니다. 보다 자세한 내용은 원 Roslyn 페이지에서 찾아 보실수 있습니다.

2 Exposing the Core APIs

로즐린의 Core API는 C#과 VB 컴파일러의 코드 분석 정보를 전통적인 컴파일러 파이프 라인과 비슷한 layer의 API를 통해 제공합니다.

위의 그림에서 각각의 파이프라인 단계 하나 하나가 콤퍼넌트가 되어, 첫 콤퍼넌트는 소스를 파싱해서 syntax tree를 주고, 그 다음것은 파싱된 소스와 reference들을 통해 심볼을 제공하고, 세번째 바인더는 소스 코드에서 심볼을 찾아주는 역할을 하고, 마지막 컴퍼넌트는 그렇게 모은 정보를 assembly로 추출하는 기능을 제공합니다.

위의 각각의 역할에 맞게, 각 컴퍼넌트마다 object model이 제공되어 집니다. 이 OB를 통해 각 단계의 정보에 접근하게 되는것이죠. 파싱 단계는 syntax tree를 제공하고, 선언 단계는 체계적인 심볼 테이블을 제공하며, 바인딩 단계는 컴파일러의 semantic 분석 정보를 제공하고, 마지막 단계는 IL을 출력하는 방법을 제공합니다.

이 전체 파이프라인과 컴퍼넌트들을 하나로 쭉 묶어서 처리하면 컴파일러가 되는것입니다.

로즐린에서 제공하는 API들이 다른 third party가 툴을 만들기에 충분한 정보를 공개하는것을 보증하게 위해, Roslyn IDE 서비스들은 로즐린에서 제공하는 API만을 가지고 만들어지게 됩니다. 예를 들어 outlining이나 formatting은 syntax tree에서 제공하는 API만을 가지고 만들어지고, navigate to 기능은 심볼 API만을 가지고 만드는 식으로 말입니다.

2.1 Key Concepts

이 섹션에서는 로즐린 프로젝트의 각 레이어의 키 콘셉트들을 알아보도록 하겠습니다.

2.2 API Layers

로즐린 프로젝트는 아래의 API들을 공개합니다 – the Compiler APIs, the Scripting APIs, the Workspace APIs, and the Services APIs.

2.2.1 Compiler APIs

이 레이어는 언어의 문법적인 부분과 semantic 부분을 포함해 전통적인 컴파일러가 처리하는 모든 정보들을 제공합니다. 예를 들어, reference라던지, 컴파일 옵션이라든지, 소스 파일들이라든지, C#/VB 파일들을 컴파일하기 위해 필요한 모든 정보를 제공합니다. 이 레이어는 C#과 VB가 비슷하지만 다른 API를 사용하게 됩니다. 이 레이어는 Visual Studio에 대한 의존성이 없습니다.

2.2.2 Scripting APIs

이 레이어는 C#이나 VB 코드를 실행하는 환경을 제공합니다. 이 레이어에는 scripting engine을 가지고 있어서 그것을 통해 C#/VB expression과 statement들을 실행할수 있게 해줍니다. 이 레이어 역시 VS에 대한 의존성이 없습니다.

Note: 이 문서는 Scripting API를 다루지 않습니다. 보다 자세한 정보를 원하시면, “Introduction to Implementing Host Commands in Script Code”를 참조 하시기 바랍니다.

2.2.3 Workspace APIs

이 레이어는 손쉽게 솔루션 전체에 대한 코드 분석과 refactoring이 가능하게 해 줍니다. 여기서 제공하는 object model을 통해 솔루션에 있는 여러 project들과 그에 관계된 정보들을 간단하게 이용하실수 있습니다. 또한 workspace object model을 통해 수작업 없이 compiler object model을 가져올수 있습니다.

이 레이어는 VS 처럼 특정 host하에서 코드 분석이나 refactoring 을 하기 위해 보통 사용되어 지지만, VS에 대한 의존성은 없습니다.

2.2.4 Services APIs

이 레이어는 인텔리센스, 리펙토링, formatting과 같은 여러 VS IDE 기능들을 서비스로 제공합니다. 이 서비스들을 통해 IDE 기능들을 사용할수도 혹은 경우에 따라 확장할수도 있습니다.

이 레이어는 VS Text Editor와 Visual Studio software development kit (SDK)에 의존성이 있습니다.

3 Working with Syntax

syntax tree는 로즐린 컴파일러 API의 가장 밑바닥 API입니다. 이 tree는 소스 코드의 lexical/syntactic 정보를 제공합니다. 이를 통해 중요한 2가지 정보를 제공하게 되는데 첫번째가 툴들이 사용자의 소스코드의 문법적 정보를 접근할수 있게 해 주고, 두번째가 tree 조작을 통해 직접적인 소스 코드 text에 대한 조작없이 코드를 재구성할수 있게 해 줍니다.

3.1 Key Concepts

이 섹션은 Syntax API의 주요 콘셉들을 알아볼것입니다.

3.1.1 Syntax Trees

Syntax tree는 컴파일, 코드 분석, 바인딩, 리펙토링, IDE 기능, 코드 생성들에 사용되는 가장 기본적이고 중요한 정보입니다. syntax tree 없이는 다른 어떤 API도 작동하지 않습니다.

신텍스 트리는 3가지 중요한 특징이 있는데, 첫번째는 이 syntax tree는 source code에 존재 했던 모든 정보를 포함하고 있다는 것입니다. 스페이스, 코멘드, preprocessor 어떤것 하나도 없어지지 않습니다. 예를 들어, 문자열도 정확히 소스에 쓰여진 그대로 보존됩니다. 이 첫번째 특징이 2번째 특징도 가능하게 해줍니다.

두번째 특징은 syntax tree는 완전히 round-trippable 하다는 것입니다. 무슨 말이냐면, tree로 부터 소스를 추출 할 경우, 그 소스는 tree가 만들어진 소스와 완전히 동일한 소스를 재 구성합니다. 이 특징을 이용하면, 단순히 tree를 조작함으로써, 소스 코드를 조작하는것과 동일한 효과를 볼수 있습니다.

세번째 특징은 syntax tree가 immutable하고 thread-safe하다는 것입니다. 무슨 뜻이나면 일단 tree가 만들어진 이후에는 절대 변하지 않는다는 것입니다. 쉽게 말해, 현 소스에 대한 snapshot이라고 생각하시면 됩니다. 따라서 일단 만들어진 tree는 여러 thread에서 아무런 lock 없이 사용하실수 있습니다. 또한 이런 tree를 조작하기 위한 여러가지 factory 함수들이 제공되어 집니다.

syntax tree는 말 그래로 중간 노드가 child 구조를 가지고 있는 tree 데이타 구조입니다. syntax tree는 노드, 토큰, 트리비아로 구성됩니다.

3.1.2 Syntax Nodes 

Syntax node는 syntax tree의 가장 기초적인 구성 요소입니다. 이 노드들은 declarations, statements, clauses, and expressions 등과 같은 문법적인 구문들을 나타냅니다. 각각의 노드들은 각자의 문법 구문에 따라 종류가 나눠지며, 모든 종류의 노드는 SyntaxNode로부터 상속된 타입입니다. 하지만, 사용자들이 특정 노드를 다시 상속함으로써 노드를 확장할수는 없습니다.

모든 syntax node들은 tree의 끝자리 노드가 아닙니다, 무슨 말이냐면, ��든 노드는 자식 노드나 토큰을 가지고 있게 됩니다. 또 모든 자식 노드들은 부모 노드에 대한 포인터를 가지고 있습니다. 루트 노드의 경우는 null을 부모 노드로 가지게 됩니다. syntax tree 와 node는 immutable 이기 때문에, 이 부모/자식간의 관계는 한번 만들어진 이후엔 바뀌지 않습니다.

각각의 노드들은 ChildNodes, DescendentNodes, DescendentTokens 와 같은 함수들을 통해 노드 아래에 있는 정보들을 여러 형태로 가져오는 방법을 지원합니다. 예를 들어, ChildNodes는 부모 노드 바로 밑에 있는 syntax node 만을 돌려주고, DescendentNodes는 부모 노드 아래에 있는 모든 노드들을 돌려주는 식으로 말입니다.

더불어, 각각의 세세한 노드 타입들은 기본적으로 다른 노드 타입들과 공유하는 정보 뿐만 아니라 그 타입만 존재하는 정보들을 제공합니다. 예를 들어 BinaryExpressionSyntax 노드는 Left, OperatorToken, Right과 같은 정보를 제공합니다. 때때로 어떤 노드들은 이렇게 항상 존재하는 정보 뿐만 아니라, 선택적으로 제공되는 정보도 제공합니다. 보통 그런 정보들은 Opt이라는 접미사를 가집니다. 예를 들어, IfStatementSyntax는 ElseClauseOpt 라는 정보를 제공합니다. 선택적인 정보의 경우, 정보가 제공되지 않을 경우 null을 값으로 가지게 됩니다. 선택적 정보가 아닌 경우에는 절대 null 값을 가지지 않습니다.

3.1.3 Syntax Tokens

Syntax token은 keywords, identifiers, literals, and punctuation과 같은 문법적 구문의 가장 끝머리를 나타냅니다. 따라서 토큰은 절대 다른 토큰이나 노드의 부모가 되지 않습니다.

로즐린에서 syntax token은 성능상의 이유로 CLR의 value type입니다. 따라서, 노드와는 다르게 토큰은 오직 한가지 종류만이 존재합니다. 대신 토큰은 토큰 종류에 따라 다른 정보를 제공하는 많은 종류의 property를 제공하게 됩니다. 예를 들어, 정수 숫자를 나타내는 integer literal 토큰의 경우 일반적인 소스 텍스트의 위치 뿐만 아니라, Value property를 통해 그 텍스트가 어떤 정수 숫자로 파서에 의해 인식되었는지도 알려줍니다. 물론 Value property가 다른 종류의 토큰에서도 같이 사용될수 있으므로 type은 안타깝게도 object입니다. 다른 예로, ValueText의 경우는 Value와 같은 정보를 항상 string으로 보여줍니다. 따라서 이것도 그냥 코드에 나와 있는 값의 string 표현이 아니라, 파서가 인식한 값의 string 표현으로 보여줍니다. 

3.1.4 Syntax Trivia

Syntax trivia는 코드상의 whitespace, comments, and preprocessor directives과 같이 프로그램상에서는 별 의미가 없지만, 툴에게는 필요할수도 있는 정보들을 포함합니다. 이 trivia는 실제 언어 문법에 포함되지 않고, 아무 토큰 사이라도 있을수 있기 때문에, syntax tree에 자식노드로는 포함되어 있지 않습니다. 하지만, 툴들이나 syntax tree의 full fidelity를 제공하기 위해 syntax tree 안에 정보가 저장은 되어 있습니다.

이 정보들은 각 토큰의 LeadingTrivia 와 TrailingTrivia를 통해 접근할수 있습니다. 소스를 parse 하는 동안 일련의 trivia들이 옆에 있는 token들과 맵핑되어 집니다. 일반적인 규칙은 같은 줄에 있는 토큰 뒤에 있는 trivia들은 그 앞에 있는 토큰에게 맵핑됩니다. 만약 2개의 토큰이 2줄에 걸쳐 있으면, 나머지 trivia들은 다음 토큰에 맵핑됩니다. 소스의 첫번째 토큰은 그 앞에 있는 모든 trivia 들과 맵핑되고 소스의 가장 마지막 토큰은 그 뒤에 있는 모든 trivia와 맵핑됩니다. 

노드와 토큰과는 다르게 trivia는 부모 property가 없습니다. 이유는 trivia는 tree에 자식으로 포함되지 않고, 연관된 토큰과 맵핑만 되기 때문입니다. 따라서 trivia는 token이라는 property를 가지고 있습니다.

토큰과 마찬가지로 trivia는 성능상의 이유로 value type입니다. 따라서 하나의 SyntaxTrivia 타입으로 여러가지 다른 종류의 trivia를 나타냅니다.

3.1.5 Spans

모든 노드와 토큰과 트리비아는 소스 코드상에서의 자신의 위치와 크기를 기억하고 있습니다. 소스상의 텍스트의 위치는 0 베이스의 유니코드 32비트 정수 인덱스로 표현되는데, 이를 위해 TextSpan이라는 타입을 사용합니다. 이 타입을 이용해 텍스트의 위치와 크기를 알수 있습니다. 만약 TextSpan이 0 크기를 가지고 있다면, 이건 2글자 사이를 나타내는것입니다.

모든 노드와 토큰은 Span과 FullSpan을 가지게 되는데, Span은 가장 처음 토큰의 시작 위치부터 가장 마지막 토큰의 끝나는 위치까지를 TextSpan으로 가지게 되고, FullSpan은 가장 첫 토큰의 맨 앞 트리비아의 첫 위치와 맨 마지막 토큰의 맨 마지막 트리비아의 끝 위치를 TextSpan으로 갖게 됩니다.

예를 들어

if (x > 3)
{
    // this is bad
    throw new Exception(“Not right.”);  // better exception?
}

블락안에 있는 statement 노드의 span은 밑줄로 표시되어 있고, full span은 노란색으로 표시 되어 있습니다.

3.1.6 Kinds

모든 노드, 토큰, 트리비아는 SyntaxKind 타입의 Kind property가 있습니다. 이를 통해 각각의 노드, 토큰, 트리비아가 어떤 문법 구문을 나타내는지 알수 있습니다. 각각의 언어마다 그 언어의 모든 노드, 토큰, 트리비아를 나타내는 SyntaxKind 타입이 있습니다.

이 Kind Property를 통해서 동일한 node 타입이 다른 구문에 쓰이는걸 구별하실수 있습니다. 토큰과 트리비아의 경우는 이 kind 프로펄티가 여러 다른 종류의 토큰과 트리비아를 구별하는 유일한 방법입니다.

예를 들어, BinaryExpressionSyntax 노드가 AddExpression인지 아니면 SubstractExpression, MultiplyExpression인지 kind property를 통해서 알수 있습니다.

3.1.7 Errors

위에서 말한 syntax tree의 round-trippable한 특성은 코드에 에러가 있는 경우도 유효합니다. 만약 파서가 소스 코드를 분석하는 동안, 문법에 맞지 않는 구문을 만난다면, 다음 2개중 하나를 하게 됩니다.

첫번째, 만약 파서가 어떤 특정한 종류의 토큰을 기다리고 있었지만, 찾지 못했다면, 파서가 빠진 부분을 missing token으로 대체하고 계속 파싱을 할것입니다. missing token은 IsMissing property를 통해 확인 할수 있으며, 0 길이의 Span을 가지게 됩니다.

두번째로, 만약 파서가 현 위치의 소스코드를 전혀 이해 할수 없다면, 파서가 이해 할수 있는 부분을 만날 때까지 이해할수 없는 부분의 토큰들을 SkippedTokens 이라는 종류의 trivia로 만들어 trivia 룰에 따라 토큰에 맵핑하게 됩니다.

3.2 Obtaining a Syntax Tree

Roslyn.Compilers와 Roslyn.Compilers.Common 안에는 공용 Syntax API가 있고, Roslyn.Compilers.CSharp와 Roslyn.Compilers.VisualBasic 안에는 각 언어 특유의 Syntax API가 있습니다. 각각에 들어 있는 API들은 각각 다음의 namespace를 이용하여 사용하실수 있습니다.

using Roslyn.Compilers;

using Roslyn.Compilers.Common;

using Roslyn.Compilers.CSharp;

using Roslyn.Compilers.VisualBasic;

가장 쉽게 소스 코드에 대한 syntax tree를 얻는 방법은 SyntaxTree.ParseCompliationUnit 함수를 사용하는 방법입니다.

예를 들어 아래와 같이 하면 됩니다.

SyntaxTree tree = SyntaxTree.ParseCompilationUnit(
                @"using System;
 
                namespace HelloWorld
                {
                    class Program
                    {
                        static void Main(string[] args)
                        {
                            Console.WriteLine(""Hello, World!"");
                        }
                    }
                }");

syntax tree 뿐만 아니라 syntax node도 유사한 방법으로 얻으실수 있습니다. Syntax 타입은 다양한 parsing 함수를 제공합니다. 이것을 통해, 전체 프로그램이 아닌 부분 이를테면 statement, expression 심지어 comment만 따로 parsing 하실수 있습니다.

ExpressionSyntax myExpression = Syntax.ParseExpression("1+1");

StatementSyntax myStatement = Syntax.ParseStatement("for (int i = 0; i < length; i++) { }");

위의 예의 경우, myExpression의 타입은 BinaryExpressionSyntax 이고 myStatement의 타입은 ForStatementSyntax 입니다.

3.3 Manipulating Syntax Nodes

Roslyn은 기존 tree를 변형 한다던지 새로운 syntax node를 삽입함으로써 좀 더 쉽게 코드를 조작할수 있는 방법을 제공합니다.

3.3.1 Creating a node

Syntax class 안에 있는 factory 함수를 이용하여 새로운 syntax node를 생성하실수 있습니다. 각각의 syntax node, token, trivia 마다 각각을 위한 factory 함수가 Syntax class안에 마련되어 있습니다.

예를 들어, “Roslyn.Compilers” 라는 namespace declaration node를 만든다고 하면, 일단 namepace keyword token과 name node가 필요한데, namespace keyword token은 Syntax.Token 함수를 통해 아래 처럼 만들수 있고,

SyntaxToken namespaceToken = Syntax.Token(SyntaxKind.NamespaceKeyword);

“Roslyn.Compilers” 라는 name node는 아래 예처럼 만드실수 있습니다.

QualifiedNameSyntax namespaceName = Syntax.QualifiedName(
                           left: Syntax.IdentifierName("Roslyn"), 
                           right: Syntax.IdentifierName("Compilers"));

위의 방식이 너무 복잡하다만, 아래와 같이 ParserName 방법을 쓰실수도 있습니다.

NameSyntax namespaceName = Syntax.ParseName("Roslyn.Compilers");

마지막으로 위에서 만든 token과 node를 이용해서 아래와 같이 namespace declaration node를 만드실수 있습니다.

NamespaceDeclarationSyntax myNamespace = Syntax.NamespaceDeclaration(
                           namespaceKeyword: namespaceToken, 
                           name: namespaceName);

만약 위의 namespace declaration node를 text로 바꾼다면 아래와 같은 text를 얻게 되실겁니다.

namespaceRoslyn.Compilers{}

안타깝게도, 위에 처럼 만들어진 text는 글자 사이에 빈공간이 없습니다. 만약 위의 text를 다시 parse 하실려고 하면, 에러가 날것입니다. 이런 경과를 원하지 않으신다면 node와 token을 만드실때 trivia를 직접 지정해 주셔야 합니다.

namespace keyword token 뒤에 아래처럼 Space trivia를 지정해 줌으로써 text를 생성시 namespace 뒤에 space가 오게 하실수 있습니다.

SyntaxToken namespaceToken = Syntax.Token(
                             SyntaxKind.NamespaceKeyword, 
                             Syntax.Space);

text로 변환 했을시 결과물입니다.

namespace Roslyn.Compilers{}

위처럼 자신이 직접 trivia를 지정하는걸 원치 않으실 경우, node를 built-in style로 아래처럼 format 하실수 있습니다.

NamespaceDeclarationSyntax formattedNamespace = myNamespace.Format();

결과는 아래와 같습니다.

namespace Roslyn.Compilers
{
}

3.3.2 Modifying an existing node

만약 어떤 분석후 기존 소스 코드를 변환 시키고 싶으실 경우, 일반적으로 완전히 새로운 node를 만들기 보단 기존에 있던 node를 변형 시키고 싶으실 것입니다. 이때 이 변형된 node를 기존 node의 Update 함수를 이용하여 얻으실수 있습니다.

예를 들어, Section 3.2에 잇는 HelloWord namespace 를 Roslyn.Compilers namespace로 update 하고 싶으실 경우, 아래와 같이 예전 namespace node를 찾으신 후

NamespaceDeclarationSyntax oldNamespace = tree.Root.DescendentNodes()
                          .OfType<NamespaceDeclarationSyntax>()
                          .First();

아래 처럼 Update 함수를 이용하여 새 namespace node를 얻으실수 있습니다.

NamespaceDeclarationSyntax newNamespace = 
			oldNamespace.Update(oldNamespace.NamespaceKeyword, 
                                       namespaceName, 
                                       oldNamespace.OpenBraceToken, 
                                       oldNamespace.Externs, 
                                       oldNamespace.Usings, 
                                       oldNamespace.Members, 
                                       oldNamespace.CloseBraceToken, 
                                       oldNamespace.SemicolonTokenOpt);

만약, 위의 새로운 namespace node를 text로 변환 하실 경우, 아래와 같은 text를 얻으실수 있습니다.

namespace Roslyn.Compilers {
       class Program
       {
             static void Main(string[] args)
             {
                    Console.WriteLine(""Hello, World!"");
             }
       }
}

3.3.3 Replacing a node

tree에서 기존의 node를 다른 node로 완전히 교체하고 싶으시다면, ReplaceNode 함수를 이용하여 아래와 같이 교체 하실수 있습니다.

SyntaxNode newRoot = tree.Root.ReplaceNode(oldNamespace, newNamespace);

만약 newRoot을 text로 변환 하신다면 아래와 같은 코드를 얻으실것입니다.

using System;
 
namespace Roslyn.Compilers
{
     class Program
     {
          static void Main(string[] args)
          {
               Console.WriteLine(""Hello, World!"");
	   }
     }
}

3.3.4 Creating a Syntax Tree

위에서처럼 tree의 한 부분을 고치실 경우, 기존 tree를 수정하신다고 생각하실수 있겠으나, 사실 tree가 immutable 하기 때문에 완전히 새로운 tree가 생성되는것입니다. 좀 더 정확히 말하면, 새로운 root node가 만들어 지는 것이죠.

만약 새로운 syntax tree가 필요하다면, 아래와 같이 새로운 tree를 만드실수 있습니다.

SyntaxTree newTree = SyntaxTree.Create(tree.Filename, newRoot, tree.Options);

4 Working with Semantics

Syntax tree가 소스의 문법적인 부분에 대한 정보를 제공해 준다면, semantic은 소스의 의미/심볼 그리고 reference들에 대한 정보를 제공해 줍니다.

예를 들어, 많은 같은 이름의 타입, 필드, 함수, 지역 변수가 전체 프로그램네 다른 scope안에서 사용되어 지는데, 이 같은 이름 중 어떤게 어떤것을 가르키는지 알기 위해선 C#/VB 언어 자체의 룰들에 대한 이해를 필요로 하게 되는데, 이때 정보를 제공해 주는것이 semantic API 입니다.

추가로, 프로그램네에서 사용되는 심볼들이 소스에서만 오는게 아니라, reference들에서도 오기 때문에, 소스의 syntax tree 만으론 전체 프로그램을 나타낼수 없게 됩니다. semantic model을 통해 이런 정보들를 제공 받으실수 있습니다.

4.1 Key Concepts

이번 section에서는 Semantic API가 어떤것이며, 어떤 정보를 제공해 주고 어떻게 사용할수 있는지에 대해 알아보겠습니다.

4.1.1 Compilation

compilation 은 하나의 C# 혹은 VB 프로그램을 compile 하기 위한 모든 것을 나타내는 concept/type입니다. 따라서, 이 compilation을 이용하여 assembly 레퍼런스, 컴파일 옵션, 소스 파일등에 대한 정보를 가져 올수 있습니다.

이 모든 정보들이 한군데에 모여 있기 때문에, 소스 파일과 연관된 여러 정보들을 제공해 줄수 있습니다. 예를 들어 compilation은 symbol이란 이름으로 그안에 정의되어 있는 모든 type, member, variable들에 대한 정보를 제공해 줄수도 있고, compilation가 제공하는 여러 API를 통해 소스에서 정의되어진 symbol간의 관계나 reference로 부터 import된 심불들 간의 관계에 대한 정보도 제공해 줄수 있습니다.

syntax tree와 유사하게, compilation 또한 immutable입니다. 따라서 일단 compilation이 만들어진 이후에는 수정 되어지지 않습니다. 역시나 syntax tree와 마찬가지로 기존 compilation으로 부터 새 compilation을 생성하실수 있습니다. 예를 들어 새로운 reference를 기존 compilation에 추가 한다던지 새로운 소스 파일 혹은 syntax tree를 추가 하는 방법으로 수정된 새 compilation을 얻으실수 있습니다.

4.1.2 Symbols

심볼은 소스에 정의 되거나 레퍼런스로 부터 import된 어떤 하나의 객체를 나타냅니다. 예를 들어, 모든 namespace, type, method, property, field, event, parameter, local variable들은 각각 하나의 심볼로 나타내어 집니다.

Compilation의 여러 API를 이용해 하나의 컴파일레이션 안에 정의된 symbol들을 찾으실수 있습니다. 예를들어, 타입 심볼을 그의 메타데이타 이름을 통해 찾으실수도 있고, 아니면 트리 형태로 된 전체 심볼 테이블을 직접 돌아다녀서 찾으실수도 있습니다.

심볼은 컴파일러에 의해 알아내어진 여러 정보들을 제공해 줍니다. 각각의 심볼들은 그 종류에 따라 여러 subclass로 나눠지는데, 각각의 subclass들은 그 종류마다 다른 정보들을 제공하게 됩니다. 예를 들어, MethodSymbol의 ReturnType property는 그 method가 return하는 TypeSymbol을 리턴해 줍니다.

또한 심볼은 다른 경로로 들어온 정보들을 (소스, reference) 하나의 형태로 보는것을 가능하게 해 줍니다. 예를 들어 소스에 정의된 함수와 assembly로 부터 import된 함수가 같은 MethodSymbol로 표현 되어 집니다.

심볼은 어떤면으론 System.Reflection API로 대표되는 CLR type system의 콘셉과 비슷하지만, 그것과는 다르게 type에 대한 정보 뿐만 아니라 namespace, local variables, labels 등 더 다양한 부분에 대한 정보 역시 제공합니다. 추가로 심볼은 C#, VB 언어를 나타내는 개념이지 CLR을 나타내는 개념이 아닙니다. 물론 둘 사이에 overlap 되는 부분도 있지만, 충분히 다른 개념을 나타내는 부분도 있습니다. 예를 들어, C#/VB의 iterator method는 Roslyn에서는 하나의 symbol 이지만, 일단 이것이 CLR metadata로 바뀐 이후엔 하나의 타입과 여러개의 method가 됩니다.

4.1.3 Semantic Model

semantic model 은 하나의 소스 파일에 있는 모든 semantic 정보를 나타냅니다. 이를 통해 다음과 같은 정보를 알아내실수 있습니다.

  • 소스의 특정 위치에 있는 심볼
  • 모든 expression의 타입
  • 파일에 있는 모든 warning과 error
  • 특정 변수가 파일을 특정 부분에 어떻게 사용되어 졌는지
  • 소스에는 없는 추측적 질문에 대한 답변
4.2 Obtaining a Compilation

compilation은 syntax tree, assembly reference, compilation option을 이용하여 생성 하실수 있습니다. compilation을 만드는 행위가 disk에 실제 assembly 파일을 만들진 않습니다. compilation은 주어진 정보에 대한 컴파일러 분석의 결과를 나타낼 뿐입니다.

아래와 같이 Create factory 함수를 이용하여 여러가지 방법으로 Compilation을 생성하실수 있습니다.

SyntaxTree tree = SyntaxTree.ParseCompilationUnit(sourceText);
 
AssemblyFileReference mscorlib = new AssemblyFileReference(
                  		      typeof(object).Assembly.Location);

Compilation compilation = Compilation.Create(
                		outputName: "HelloWorld",
                		syntaxTrees: new [] { tree },
                		references: new MetadataReference[] {
                                          mscorlib });

Compilation compilation = Compilation.Create("HelloWorld")
 		  		.AddReferences(mscorlib)
		  		.AddSyntaxTrees(tree);

위의 2가지 방법 중 아래 방법은 임시 compilation을 생성하게 되지만, 그것이 특별히 성능에 영향을 주진 않습니다. 모든 실제 분석 작업은 실제 그 정보가 필요해 질때 까지 연기 되어 집니다.

4.3 Obtaining Symbols

compilation과 symbol에 있는 여러 함수와 property를 이용해서 compilation안에 있는 모든 symbol들을 검색하실수 있습니다. compilation 안에 symbol table은 tree 형태로 저장되어 있고, 그 가장 root은 글로벌 네임스페이스 입니다. 그리고 그 글로벌 네임스페이스는 아래와 같은 방법으로 얻으실수 있습니다.

NamespaceSymbol globalNamespace = compilation.GlobalNamespace;

이 네임스페이스 심볼로 부터, 그 아래있는 자식 네임스페이스 혹은 타입과 같은 모든 멤버들을 얻으실수 있습니다.

foreach (Symbol member in globalNamespace.GetMembers())
{
	// Process member
}

이렇게 모든 멤버를 다 찾는 방식 말고, 특정 이름을 가진 멤버만 찾는 방법도 있습니다. 일반적으로 특정 이름을 가진 멤버만 찾는게, 모든 멤버를 가져와서 원하는 이름을 가진 멤버만 따로 추수리는것 보다 효율적입니다. 아래는 “System.String”이란 이름을 가진 멤버만 찾는 예입니다.

NamespaceSymbol systemNamespace = globalNamespace.GetMembers("System")
				       .First() as NamespaceSymbol;

NamedTypeSymbol stringType = systemNamespace.GetMembers("String")
				  .First() as NamedTypeSymbol;

또 다른 방법은 CLR metadata에 사용되어지는 매타데이타 이름을 사용하여 심볼을 찾는 방식입니다. 아래는 그 간단한 예입니다.

NamedTypeSymbol stringType = compilation.GetTypeByMetadataName(
				  "System.String");

타입 심볼로 부턴 field, method, property, event, nested type과 같은 모든 멤버들 정보를 얻으실수 있습니다.

foreach (Symbol member in stringType.GetMembers())
{
    // Process member
}

이밖에도 심볼이 제공하는 다른 property와 함수들을 이용하여 연관된 정보들을 얻으실수 있습니다. 예를 들어 밑의 코드는 string type symbol을 이용하여 string type의 base type과 interface 정보를 알아내는 예입니다.

NamedTypeSymbol baseType = stringType.BaseType;

foreach (NamedTypeSymbol i in stringType.Interfaces) { /* ... */ }
4.4 Asking Semantic Questions

SemanticModel이라는 타입을 이용하여 하나의 소스 파일에 대한 여러 semantic에 관계된 정보 알아내실수 있습니다. 예를 들어 어떤 expression의 결과 타입이라든지, invocation에 사용된 method symbol이라든지. 이런 정보를 SemanticModel를 통해 얻으실수 있습니다.

이 SemanticModel은 compilation으로부터 아래와 같은 방법을 이용하여 얻으실수 있습니다.

SemanticModel semanticModel = compilation.GetSemanticModel(tree);

위에 사용된 tree는 반드시 compilation을 만들때 사용되어진 syntax tree중 하나 이어야 합니다. 이 방법 이외에 Section 5에 나와 있는 IDocument를 이용해서도 SemanticModel을 얻으실수도 있습니다.

일단 SemanticModel을 얻으셨으면, 아래와 같이 여러가지 정보를 Model로 부터 알아내실수 있습니다. 첫번째 예는 method declaration node를 이용하여 method symbol을 찾아내는 방법입니다.

MethodDeclarationSyntax methodDecl = tree.Root
					  .DescendentNodes()
					  .OfType<MethodDeclarationSyntax>()
					  .First();
                                  
Symbol methodSymbol = semanticModel.GetDeclaredSymbol(methodDecl);

또 다른 예는 expression node로부터 그 결과 type을 알아내는 예제 입니다.

ExpressionSyntax expression = methodDecl.DescendentNodes()
			    	  .OfType<ExpressionSyntax>()
			    	  .First();
                  
SemanticInfo info = semanticModel.GetSemanticInfo(expression);

TypeSymbol resultantType = info.Type;

특정 invocation에 컴파일 타임에 bind된 method가 무엇인지도 아래와 같이 알아내실수 있습니다

InvocationExpressionSyntax invocation = expression.DescendentNodes()
				      .OfType<InvocationExpressionSyntax>()
				      .First();
                  
SemanticInfo info = semanticModel.GetSemanticInfo(invocation);

Symbol invokedSymbol = info.Symbol;

여기서 한가지 알아두셔야 할것은 SemanticInfo 타입의 Symbol과 Type property는 서로 다른 의미의 정보라는것입니다. Type property는 주어진 expression의 결과 타입을 나타내고, Symbol property는 expression에 의해 참조된 하나의 심볼을 나타냅니다. 어떤 expression은 Type은 있지만 Symbol은 없을수 있고, 어떤 expression은 둘 다 있을수 있습니다. 예를 들어 invocation expression은 두 정보를 다 가지게 되는데, Symbol은 invoke된 method symbol을 나타내게 되고, Type은 그 method의 return type이 되게 됩니다.

만약, 소스에는 없는 expression의 결과 type을 특정 소스의 위치에서 알아보고 싶다면, BindExpression과 같은 speculatvie 함수를 쓰시면 됩니다.

ExpressionSyntax otherExpression = Syntax.ParseExpression("X.Y");
int location = methodDecl.BodyOpt.Span.Start;
                
SemanticInfo info = semanticModel.BindExpression(location,
			  			      otherExpression);
TypeSymbol resultantType = info.Type;

이 외에 BindType과 같은 추측함수를 이용하여 특정 type 이름이 소스의 특정 위치에서 어떤 type으로 bind되는지도 알아보실수 있습니다.

ExpressionSyntax typeExpression = Syntax.ParseTypeName("System.String");
int location = methodDecl.BodyOpt.Span.Start;
        
SemanticInfo info = semanticModel.BindType(location, typeExpression);

TypeSymbol resultantType = info.Symbol;
4.5 Working with Fault Tolerance

일반적으로 사용자가 코드중에 사용하는 툴을 만드는 경우, 그 툴이 분석하는 코드에 에러가 없을 확율은 거의 없습니다. 대부분의 경우, 이런 툴들은 여러가지 문법적 에러와 애매모호한 상황을 처리할수 있어야 합니다.

예를 들어, 에러 혹은 여러가지로 해석 될수 있는 상황에서 semantic model은 best guess 결과 타입을 제공해 줌으로써, 이런 상황에서 조차 사용자의 코드를 분석할수 있는 방법을 제공해 줍니다. 이를테면, 어떤 method invocation이 2개 이상의 overload로 bind 가능할 경우, 어떤 하나의 method를 고르지는 못하지만, 만약 둘이 같은 return type을 가지고 있다면, 그 type을 method invocation의 결과 type으로 돌려주는것입니다.

또 다른 예로, 위의 경우 bind 가능한 모든 symbol들을 아래처럼 후보자로 돌려줍니다.

InvocationExpressionSyntax invocation = expression.DescendentNodes()
				.OfType<InvocationExpressionSyntax>()
				.First();
 
SemanticInfo info = semanticModel.GetSemanticInfo(invocation);
Symbol invokedSymbol = info.Symbol;

if (invokedSymbol == null)
{
    ReadOnlyArray<Symbol> candidates = info.CandidateSymbols;
    CandidateReason reason = info.CandidateReason;
    ...
}

위의 CandidateSymbols property는 후보자들의 리스트를 돌려주고, CandidateReason은 왜 하나의 symbol로 bind되지 못했나 이유를 알려줍니다.

4.6 Using the Control and Data Flow Analysis APIs

만약 특정 소스 구역에 어떤 로컬 변수가 사용��어지고, 변수 값이 어떻게 이용되어 졌는지 알고 싶다면, Data Flow Analysis API를 사용하시면 됩니다. 또한 Control Flow Analysis API를 사용하시면, 어떤 statement이 어떻게 콘트롤 flow를 바꾸었는지 알아내실수 있습니다.

control flow 정보는 아래와 같이 semantic model의 AnalyzeRegionControlFlow 함수를 이용해 알아내실수 있습니다.

TextSpan span = methodDecl.BodyOpt.Span;    
             
RegionControlFlowAnalysis controlFlow =
			        semanticModel.AnalyzeRegionControlFlow(span);

구역안에 사용된 return statement를 알아내고 싶으시면 아래와 같이 하시면 됩니다.

var returns = controlFlow.ReturnStatements;

현 구역에서 jump out하게 만드는 statement들은 아래와 같이 알아내실수 있고

var jumpsOut = controlFlow.JumpsOutOfRegion;

현 구역으로 jump in하게 만드는 statement들은 아래와 같이 알아내실수 있습니다.

var jumpsIn = controlFlow.JumpsIntoRegion;

만약 구역안에서 사용된 변수들에 대한 정보를 알고 싶으시다면, semantic model의 AnalyzeRegionDataFlow 함수를 사용하시면 됩니다.

RegionDataFlowAnalysis dataFlow =
			    semanticModel.AnalyzeRegionDataFlow(span);

구역안에서 정의된 로컬 변수는 이렇게 알아내실수 있고

var declared = dataFlow.VariablesDeclared;

구역 밖에서 정의되고 assign된 변수가 구역안 read됐는지는 이렇게 아실수 있고

var usedInside = dataFlow.DataFlowsIn;

구역안에서 assign된 변수가 구역밖에서 read됐는지는 이렇게 아실수 있습니다.

var usedOutside = dataFlow.DataFlowsOut;

이 밖에도 여러 정보를 data/control flow analysis API를 통해 알아내실수 있습니다.

5 Working with a Workspace

workspace layer 야 말로 VS 전체 솔루션에 대한 분석과 refactoring을 쉽게 가능하게 해 주는 layer입니다. workspace는 하나의 솔수션의 모든 project와 그와 연관된 정보들을 하나의 object model로 표현해 줍니다. 또한 workspace object model로부터 자연스럽게 compiler layer의 syntax tree, semantic model, compilation과 같은 object model들을 가져오는 방법들을 제공합니다. workspace API를 이용하면 사용자가 직접 코드 소스를 parse하거나, compilation option을 설정하거나, assembly reference들을 설정하거나 할 필요 없이 workspace layer가 알아서 다 처리해 줍니다.

이 workspace는 IDE와 같은 Host 환경이 현재 host에 load된 solution에 맞춰 생성하게 됩니다. 하지만 그렇다고 이게 IDE와 같은 Host에서만 사용할수 있는것은 아닙니다. VS sln파일을 이용하여 workspace를 바로 생성하여 Console에서 사용할수도 있습니다.

5.1 Key Concepts

이 섹션에서는 workspace api에 대한 콘셉과 이를 이용하여 얼마나 쉽게 solution, project, document를 다룰수 있는지 알아볼것입니다.

5.1.1 Workspace

workspace는 host 환경과 타이트하게 연결되어져 있는, 사용자의 host와의 상호작용(typing과 같은)에 따라 끊임없이 변하는 solution, project, document의 live reprentation입니다.

workspace는 밑에 설명되어 있는 그 만의 object model을 통해 현 host의 상태를 제공합니다. host와 사용자의 상호작용이 일어날때 마다, workspace는 그에 상응하는 event를 fire하고, CurrentSolution property를 업데이트합니다. 예를 들어, 사용자가 host의 에디터에 타입을 했다면, workspace는 수정된 document와 기타 정보를 event를 통해 알려주고, 그 정보를 듣고 있는 툴들은 그 change 정보 혹은 current solution을 바탕으로 새로운 코드 분석과 여러가지 작업을 하게 되는것입니다.

그러나, workspace가 꼭 host 환경에서만 사용될수 있는것은 아닙니다. 호스트 환경 없는 stand-alone한 workspace를 만드실수도 있습니다.

5.1.2 Solutions, Projects, Documents

비록 workspace는 사용자와 host간에 상호작용이 있을때 마다 변하지만, solution object model은 그와 상관 없이 독립적으로 이용되어질수 있습니다.

solution object model은 다른 layer의 object model과 마찬가지로 immutable 합니다. 단지 이번엔 solution, project, document 들에 대한 정보를 제공합니다. 역시나 다른 object model과 마찬가지로, CurrentSolution property를 통해 얻은 solution은 아무런 lock 없이 여러 thread에서 사용하실수 있고, 기존 solution에 change를 적용함으로써 새로운 solution을 얻을수 있습니다. 나중에 이 새로운 solution을 workspace에 적용함으로써 change를 실제 host에 적용할수도 있습니다.

project model은 immutable한 solution model의 project를 나타내는 한 부분입니다. 이 프로젝트 모델은 project가 가지고 있는 소스 코드, parse와 compilation 옵션, assembly와 project-to-project 참조와 같은 모든 연관된 정보를 제공합니다. 이 project model로 부터 이 project 와 대응하는 compiler layer의 compilation object model을 가져 오실수 있습니다.

document model 역시 immutable한 solution model의 하나의 부분으로써, 하나의 소스 코드 파일을 나타냅니다. 이를통해 실제 소스 코드, syntax tree 그리고 semantic model를 얻을수 있습니다.

밑에 그림은 workspace layer가 여러 다른 layer와 host와 어떤 식으로 연결되어 있으며, 소스 코드에 edit이 일어날 경우 어떤식으로 처리가 되는지 간단하게 표현해 놓은것입니다.

5.2 Obtaining a Workspace

Workspace API는 Roslyn.Service namespace를 using 함으로써 사용하실수 있습니다.

using Roslyn.Services;

일반적으로 workspace는 VS IDE와 같은 host 환경으로 부터 제공되지만, 원하신다면 아래와 같이 host 환경 없이 본인이 직접 만들거나 VS sln을 load하여 사용하실수 있습니다.

IWorkspace workspace = Workspace.LoadSolution(@"HelloWorld.sln");
ISolution solution = workspace.CurrentSolution;

host 환경에서 제공되는 workspace의 경우 host 가 알아서 특정 이벤트에 따라 update 시켜주지만, 위의 경우처럼 독립적으로 workspace를 만들었을 경우, 그 workspace는 본인이 직접 update 하지 않는한 스스로 외부 event에 따라 수정 되지 않습니다.

Solution도 마찬가지로, 직접 독립적으로 load 하실수 있습니다.

ISolution solution = Solution.Load(@"HelloWorld.sln");

이 방법 말고도 원하신다면, emtpy solution으로부터 직접 원하시는 solution을 만드실 수도 있습니다.

ProjectId pid1, pid2;
ISolution solution = 
    Solution.Create(SolutionId.CreateNewId("Solution"))
    .AddCSharpProject("Project1.dll", "Project1", out pid1)
    .AddDocument(pid1, "A.cs", "public class A { public int X; }")
    .AddCSharpProject("Project2.dll", "Project2", out pid2)
    .AddDocument(pid2, "B.cs", "class B : A { public string Y; }")
    .AddProjectReference(pid2, pid1);
5.3 Working with a Solution

solution은 어떤 한 순간의 host에 있는 모든 project와 document 정보의 snapshot입니다.

solution은 IProject들을 가지고 있는데 이 정보는 아래와 같은 방법들로 가져오실수 있습니다.

ProjectId를 아실때 이를 이용한 방법

IProject project = solution.GetProject(projectId);

project의 assembly 이름을 아실때 쓸수 있는 방법

IProject project = solution.GetProjectByAssemblyName("Project1.dll");

solution 안에 있는 모든 IProject들을 가져오는 방법

foreach (IProject project in solution.Projects) { ... }

이 각각의 project는 또 IDocument들을 가지고 있는데, 이 정보는 아래와 같은 방법으로 가져올수 있습니다.

DocumentId를 알때

IDocument document = project.GetDocument(docId);

project안에 있는 모든 IDocument들을 가져 올때

foreach (IDocument document in project.Documents) { ... }

이 document들을 통해 각 소스 파일을 여러 정보를 가져 올수 있습니다. 예를 들어 소스 코드는 아래와 같이 가져올수 있고

IText text = document.GetText();

소스 코드에 대응하는 syntax tree는 아래와 같이 가져올수 있고

CommonSyntaxTree syntax = document.GetSyntaxTree();

semantic model은 이렇게 가져 올수 있습니다.

ISemanticModel semanticModel = document.GetSemanticModel();
5.4 Updating a Workspace

만약 코드 분석후, 실제 host안의 소스 코드를 수정하려면, 결국 끝에는 workspace를 update해야 합니다. workspace를 update 하기 위해선 수정된 소스 코드를 가지고 있는 solution을 만들어야 합니다.

수정된 소스 코드를 가지고 있는 새 solution을 만드는 한가지 방법은 아래와 같습니다.

IText newText = newRoot.GetFullTextAsIText();
ISolution newSolution = solution.UpdateDocument(docId, newText);

일단 새 solution을 만들었으면, 이 solution을 이용하여 다른 분석을 또 할수도 있고, 아니면 이 새 solution에 들어 있는 change들을 workspace에 적용할수도 있습니다. 기존 solution을 이용해서 새 solution을 만드는게 자동으로 workspace를 수정하진 않습니다.

적용하길 원하시면 아래처럼 직접적으로 change들을 apply 하셔야 합니다.

workspace.ApplyChanges(solution, newSolution);

적용하실때 반드시, workspace 에서 얻은 원본 solution과 새 solution을 같이 제공해야 합니다.

6 Working with Services

지금까지 로즐린 compiler와 workspace API에 대해 알아보았습니다. 컴파일러와 워크스페이스 API 만 있으면 코드 분석과 코드 수정이 가능합니다. 하지만 많은 경우, 사람들은 VS extension을 만들거나, VS C#, VB IDE 기능들을 확장하길 원합니다. Service API는 이걸 쉽게 가능하게 해 줍니다. Service API를 통해 intellisense나 smart tag 혹은 error report를 확장하게 해 줍니다.

기타 layer와 다르게 Services APIs는 VS와 WPF editor에 의존적입니다.

6.1 Key Concepts

이번 섹션에서는 service API를 통해 refactoring, find all references, formatting, intellisense와 같은 서비스를 이용하고 확장하는 주요 콘셉에 대해 알아보겠습니다.

6.1.1 Managed Extensibility Framework

Service API는 VS extension을 이용하기 위해 MEF라고 알려진 Managed Extensibility Framework 에 의존적입니다. MEF는 가볍고 확장 가능한 어플리케이션을 만들기 위한 라이브러리 입니다. application 개발자들은 MEF를 이용하여 아무런 configuration 없이 extensions들을 찾아내고 사용할수 있습니다. 또한 이를통해 extension 개발자들은 본인의 코드를 다른 코드에 의존성 없이 개발 할수 있으며, extension들이 application 안에서 다양한 형태로 재 사용 가능하게 해 줍니다. 또한 같은 extension이 다른 application에서도 재 사용 가능하도록 해 줍니다.

6.2 Creating an IDE extension

Services API는 Roslyn.Services.Editor namespace를 using 함으로써 사용하실수 있습니다.

using Roslyn.Services.Editor;

VS IDE 확장을 지원하기 위해서, Service API 는 여러 IDE 기능에 대응하는 확장 point를 제공합니다. completion list, code outliner, highlight references, syntax classifier 그리고 code refactoring 같은게 예가 되겠습니다. 이 각각의 기능들에 대해, 사용자들은 원하는 확장 기능에 대한 정보를 담은 Export attribute을 가진 MEF provider를 제공함으로써 IDE 기능을 확장 하실수 있습니다.

예로, 만약 본인만의 code refactoring을 만드시고 싶으시다면, code issue provider를 제공함으로써 자연스럽게 기존 IDE의 기능과 함께 자신이 만든 기능이 IDE의 error list, user suggestion에 나오도록 하실수 있습니다.

code issue provider의 MEF export를 예로 보면

[ExportSyntaxNodeCodeIssueProvider("FirstQuickFixCS", 
		LanguageNames.CSharp, typeof(VariableDeclarationSyntax))]
class CodeIssueProvider : ICodeIssueProvider {}

LanguageNames.CSharp를 통해 이 code issue provider가 C# 파일에서만 작용하게 할수 있고, VariableDeclarationSyntax를 통해 이 code issue provider가 VariableDeclarationSyntax node에서만 GetIssues가 불리게 할수 있습니다.

모든 code issue provider들은 아래와 같은 ICodeIssueProvider 인터페이스를 구현해야 하는데, 이 interface에는 노드, 토큰, 트리비아를 위한 각각의 GetIssues 함수가 있습니다. 위의 export에 사용된 attribute에 따라, Roslyn의 Code Issue Engine이 알아서 맞는 GetIssues를 불러주게 됩니다.이 경우는 ExportSyntaxNodeCodeIssueProvider가 사용됐으므로 노드를 위한 GetIssues가 불려지게 됩니다.

public interface ICodeIssueProvider
{
	IEnumerable<CodeIssue> GetIssues(
               IDocument document, 
               CommonSyntaxNode node, 
 		     CancellationToken cancellationToken);
}

원할 경우, CodeIssue는 issue를 고칠수 있는 code action을 제공할수 있는데, 밑의 ICodeAction을 구현 함으로써 code action을 만들수 있습니다.

public interface ICodeAction
{
    string Description { get; }
    ImageSource Icon { get; }
    ICodeActionEdit GetEdit(CancellationToken cancellationToken);
}

위의 여러 property를 구현 함으로써, code issue 툴팁, 에러 리스트, preview에 나오는 다양한 정보들을 구현 하실수 있습니다. Services API에 대한 보다 자세한 내용은 “How to Write a Quick Fix walkthrough” 와 Roslyn CTP에 들어 있는 샘플을 보시기 바랍니다.

7 Summary

프로젝트 로질린은 Compiler APIs, Scripting APIs, Workspace APIs, and Services APIs 와 같은 다양한 형태의 API들을 제공합니다. 이 API들을 통해 C#/VB 언어에 가장 근접한 형태로 소스에 대한 다양한 정보를 제공해 줍니다. compiler를 하나의 service (API) 형태로 제공함으로써, third party들이 코드에 관계된 툴과 application을 만드는데 장벽이 됐던 여러 요소들을 많이 낮춰 줍니다. 로즐린의 API를 통해 meta-programming, code 생성과 변환, C#과 VB의 interactive한 사용, C#과 VB를 다른 domain specific language에 임베드해서 사용등등 많은 innovation이 가능해질수 있는 첫문을 열었습니다.

 

…………………………………

 

위에까지가 번역입니다. 보시면 아시겠지만, 위의 내용은 정말 대략적입니다. 위의 내용중에 구체적인 내용을 아시고 싶으신걸 적으시면 제가 그 부분에 대한 더 자세한 blog를 올리도록 하겠습니다.

 

수고

- 희제

Comments (1)
  1. 난다고래 says:

    고생하셨네요. 감사합니다. 덕분에 수월하게 보겠네요.. ^^*

Comments are closed.

Skip to main content