티스토리 뷰

이번 시간에는 로슬린을 이용해서 "상수로 만들 수 있는 변수 선언에 const 키워드를 제안하는 프로젝트" 를 만들어 보겠습니다.

풀어서 설명하면 const 키워드를 추가할 수 있는 변수 선언권고 사항을 나타내주고

클릭 한 번으로 직접 const를 추가할 수 있도록 해주는 프로젝트입니다.

이 포스팅을 다 읽고 나면 Roslyn을 이용해 "특정 구문을 찾아내는 방법""코드로 새로운 코드를 만드는 법"을 배우게 될 것입니다.

 

참고한 튜토리얼 문서는 아래와 같습니다.

https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix

 

Tutorial: Write your first analyzer and code fix

This tutorial provides step-by-step instructions to build an analyzer and code fix using the .NET Compiler SDK (Roslyn APIs).

docs.microsoft.com

 

이 프로젝트를 익히고 나면 Roslyn이 어떤 구성으로 돌아가는지 파악할 수 있게 됩니다.

Syntax Tree에 대한 이해가 있는 상황에서 보는 것이 좋긴 하지만 모르더라도 이해하는데 큰 문제는 없을겁니다.

 

로슬린을 사용하기 위해서 가장 먼저 해야할 것은 개발 환경을 셋팅하는 것입니다.

로슬린은 Visual Studio Installer를 통해서 설치할 수 있습니다.

 

1. Visual Studio Installer를 실행합니다.

2. 주로 사용하는 Visual Studio 버전의 수정 버튼 클릭합니다.

3. Visual Studio extension development를 클릭 및 수정합니다.

4. 다운로드 후 새 프로젝트에서 Analyzer with Code Fix (.NET Standard) 선택합니다.

5. MakeConst라는 이름으로 프로젝트 생성합니다.

 

처음 켜지는 솔루션은 5개의 프로젝트로 구성됩니다.

프로젝트에는 각각 간단한 예제 코드가 존재하고 있습니다.

해당 코드들을 살짝 수정하면 원래 목표하던 기능을 구현할 수 있습니다.

각 프로젝트에 대해서 간단하게 설명을 하자면 아래와 같습니다.

 

MakeConst: 가장 중요한 Analyzer를 담고 있는 프로젝트입니다,

코드를 분석하고 특정 조건에 만족했을 때 Report하는 역할을 합니다.

MakeConst.CodeFixes: 코드를 수정하는 방법을 정의하고 있습니다.

MakeConst.Package: Nuget Package 생성을 위한 것이라고 합니다.

MakeConst.Test: 유닛 테스트를 모아두는 프로젝트, Roslyn이 프로젝트의 소스 코드들을 분석해주는 것인데 디버깅을 할 때 매 번 프로젝트를 키면 오래 걸리기 때문에 간단한 소스 코드를 통해 디버깅 할 수 있게 해주는 프로젝트입니다.

다음 포스팅에서 직접 사용해볼 예정입니다.

MakeConst.Vsix: 지금까지 구현할 것을 테스트 할 때 새로운 프로젝트를 켜주는 프로젝트입니다.

이 솔루션의 시작 프로젝트로 설정하고 ctrl + f5를 누르면 새로운 프로젝트가 켜지게 됩니다.

 

구현할 기능 요약

앞으로 구현할 기능을 요약하자면 다음과 같습니다.

1
2
3
int x = 0;
 
Console.WriteLine(x);
cs

위와 같은 코드가 있고 위 코드를 아래와 같이 const를 추가하도록 바꿔주는 것입니다.

1
2
3
const int x = 0;
 
Console.WriteLine(x); 
cs

이렇게 바꿔주려면 두 가지 조건을 만족해야합니다.

1) 변수 선언 부에 값을 할당하는 것이 있어야 하며

2) 값을 다시 쓰는 부분이 없어야 합니다.

위 예시는 두 가지 조건을 모두 충족하고 있기 때문에 바꿔 줄 수 있었습니다.

 

Analyzer

어쨌든 위에서 이야기한 기능을 구현하기 위해서는 AnalyzerCodeFixer를 조합해야 합니다.

먼저 Analyzer부터 설명하면 Analyzer가 하는 일은 2가지입니다.

각각은 어떤 Syntax일 때 분석을 시도할 것인지 정의하는 것어떤 분석을 시도해서 diagnostic report를 할 것인지 정의하는 것입니다.

 

가장 먼저 봐야할 것은 MakeConst 프로젝트의 MakeConstAnalyzer.cs 파일입니다.

MakeConstAnalyzerDiagnosticAnalyzer를 상속 받고 있고 Initialize 함수를 재정의하고 있습니다.

 

처음 만들어진 프로젝트에서는 예시로 소문자가 있는 NamedType Sysmboldiagnostic report를 하고 있습니다.

NamedType Symbolclass 같은 것을 뜻합니다.

예를 들어 class Abc {} 이런 식으로 소문자가 있는 이름으로 class를 만들면 diagnostic reporting을 하게 됩니다.

 

어찌됐든 우리가 하려는 것은 const를 붙여주는 것이므로 다른 것은 무시하고 Initialize 함수의

1
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
cs

부분을 아래와 같이 수정합니다.

1
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);
cs

처음 수정을 하고 나면 컴파일 에러가 뜨게 됩니다. AnalyzeNode 함수가 정의되지 않았기 때문인데 클래스 내부에 다음과 같이 함수를 정의하면 컴파일 에러는 일단 사라지게 됩니다.

1
2
3
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}
cs

지금 한 것은 context.RegisterSyntaxNodeAction()을 통해 어떤 Syntax에 분석할 것인지와 어떤 분석을 할 것인지를 정하는 것입니다.

예를 들어 위 예제에서는 LocalDeclarationStatement를 뜻하는 Syntax Node에 분석을 할 것이고

AnalyzeNode 함수 내부에서 정의된 방식으로 분석을 할 것이라고 정하는 것입니다.

LocalDeclarationStatement라는 Syntax Node는 지역 변수로 선언한 문장을 뜻합니다.

예를 들면 함수 내부에서 변수를 선언하는 int a = 10; 같은 구문입니다.

 

다음은 AnalyzeNode 함수 내부를 정의할 차례입니다.

함수 이름 그대로 Syntax Node를 분석하는 것이므로 먼저 해당 Syntax Node를 가져와야 합니다.

해당 Syntax Node는 인자로 받는 SyntaxNodeAnalysisContext 타입 변수인 contextcontext.Node 통해 가져올 수 있습니다.

 

다음은 AnalyzeNode 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
    // local declatation statement가 바뀔 때만 호출되기 때문에 이 casting은 무조건 성공함
    var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;
 
    // 변수 선언에서 const 키워드가 이미 있을 경우 return
    if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
    {
        return;
    }
 
    // 해당 선언에 대해 data flow analysis를 진행함
    var dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
 
    // 해당 declaration의 변수들의 symbol을 가져옴
    // 해당 declaration이 하나의 변수만 선언하고 있으므로 하나만 가져옴
    var variable = localDeclaration.Declaration.Variables.Single();
    var variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
 
    // 선언 외의 부분에서 할당이 있으면 return
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
        return;
 
    // 특정 규칙에 맞는 경우 diagnostic을 report
    context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation()));
}
cs

위 코드를 쭉 읽어서 분석해 보면 결국 AnalyzeNode수에서는 Declaration에 const 키워드가 없고 블록 내에서 재할당이 없을 때 diagnostic을 리포트합니다.

중간 중간 Modifiers에서 확인해서 const 키워드가 이미 있거나 dataFlow 분석을 통해 선언 외의 할당 부분이 있는 경우 return해서 diagnostic report를 하지 않게 됩니다.

조건에 걸리지 않아서 context.ReportDiagnostic 함수 호출까지 하게 되면 비로소 Analyzer의 역할은 끝나게 됩니다.

 

CodeFixer

CodeFixer는 이름답게 새로운 Syntax를 제안하고 그 Syntax로 코드를 수정해주는 일을 합니다.

CodeFixerAnalyzer와 마찬가지로 크게 2가지 일을 합니다.

하나는 특정 Diagnostic에 특정 Code Fix Action을 연결시켜주는 것이고 다른 하나는 Code Fix Action을 정의하는 것입니다.

코드로는 MakeConst.CodeFixes 프로젝트의 MakeConstCodeFixeProvider 클래스의 RegisterCodeFixedAsync 함수를 보면 됩니다.

MakeConstCodeFixeProvider는 역시 Analyzer와 마찬가지로 CodeFixProvider를 상속 받고 있습니다.

DiagnosticCode Fix Action을 연결시켜주는 것은 RegisterCodeFixesAsync 함수를 override함으로써 구현합니다.

RegisterCodeFixedAsync 함수를 크게 수정할 부분은 없습니다.

declaration 변수를 만들 때 LocalDeclarationStatementSyntax 타입을 찾도록하고 CodeAction.Create를 호출할 때 인자로 createChangedSolution 대신 createChangedDocument의 값을 전달하며 람다식 안에서는 MakeUppercaseAsync  함수 호출 대신 이따가 선언할 함수인 MakeConstAsync를 호출하도록 수정하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
 
// report된 diagnostic들을 가져옴
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
 
// diagnostic이 발생한 declaration을 찾음
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();
 
// code fix를 하는 것을 제공함, CodeAction이라고 부름
// report된 diagnostic에 code fix를 연결
context.RegisterCodeFix(
   CodeAction.Create(
    title: CodeFixResources.CodeFixTitle,
     createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
     equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
   diagnostic);
cs

MakeConstAsync 함수는 MakeUppercaseAsync 함수와 다르게 DocumentLocalDeclarationStatementSyntaxCancellationToken을 인자로 받습니다.

MakeConstAsync 함수를 아래와 같이 구현해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private async Task<Document> MakeConstAsync(Document document, LocalDeclarationStatementSyntax localDeclaration, CancellationToken cancellationToken)
{
    // local declaration에서 leading trivia를 제거함
    var firstToken = localDeclaration.GetFirstToken();
    var leadingTrivia = firstToken.LeadingTrivia;
    var trimmedLocal = localDeclaration.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));
 
    // const token을 생성
    var constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));
 
    // const token을 local declaration의 modifier list에 삽입해서 새로운 modifier list를 만듦
    var newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
 
    // 새로운 local declaration을 만듦
    LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers).WithDeclaration(localDeclaration.Declaration);
 
    var formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);
 
    // 현재 document에 대한 handle을 가져옴
    // 해당 handle에서 local declaration 부분을 새로운 declaration으로 바꿔서 새로운 document를 만듦
    var oldRoot = await document.GetSyntaxRootAsync(cancellationToken);
    var newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);
 
    // 새로운 document의 root를 return함
    return document.WithSyntaxRoot(newRoot);
}
cs

Formatter.Annotation 부분에서 에러가 날텐데 위쪽에 이와 같이 namespace를 추가해주면 됩니다.

함수 내부 로직에 대한 설명은 코드에 주석으로 되어있습니다.

간략하게 나마 설명하면 원래의 declaration 구문을 가져와서 const token을 생성한 새로운 declaration 구문을 생성합니다.

그후 원래의 document에서 해당 declaration 부분을 찾아서 새로운 declaration으로 바꿔주는 방식입니다.

MakeConstAsync 함수를 구현함으로써 의도했던 기능은 모두 구현했습니다.

이제 프로젝트를 실행시켜 기능이 제대로 동작하는지 한 번 확인해봅시다.

 

먼저 MakeConst.Vsix를 시작프로젝트로 설정하고 ctrl + F5를 눌러서 빌드 및 실행을 합시다.

그러면 새로 Visual Studio가 켜지게 됩니다.

새 프로젝트 만들기 -> 콘솔 애플리케이션을 해서 테스트 프로젝트를 만듭니다.

그 후 실행이 되면 기존과 별반 다를바 없는 Visual Studio가 켜집니다.

하지만 자세히 보면 오른쪽 상단에 ROSLYN이라고 적힌 것을 볼 수 있습니다.

이제 이 프로젝트에서 코드를 수정해 봅시다.

위 이미지처럼 변수 선언을 하게 되면 의도했던 상황에 맞기 때문에 Diagnostic Report가 나오게 되고 마우스를 가져다 대면 자세한 설명이 나옵니다.

ctrl + .를 잠재적 수정 사항을 누르면 위 이미지와 같이 const를 붙인 제안이 나옵니다.

이 과정을 통해 Roslyn이 돌아가는 과정을 대략적으로 알 수 있었습니다.

 

매번 위 과정처럼 새 프로젝트를 실행시켜서 수정한 것을 확인해 볼 수 있으나 이렇게 할 경우 시간이 오래 걸리게 됩니다.

따라서 Unit Test라는 것이 존재하는데 이것은 말 그대로 가장 작은 단위의 테스트를 뜻합니다.

이 테스트를 잘 이용하면 의도하고 있는 기능이 제대로 구현되었는지 쉽고 정확하게 확인할 수 있죠.

다른 프로젝트에서도 많이 쓰이지만 Roslyn 특성상 Unit Test를 하기 좋습니다.

 

다음 포스팅에서는 이 Unit Test를 이용해서 특정 구문에 대해서 잘 동작하는지 확인하는 것을 해보겠습니다.

'프로그래밍' 카테고리의 다른 글

로슬린 MakeConst 튜토리얼 (2/2)  (0) 2022.01.26
C#의 StackTrace  (0) 2021.08.27
C#의 Pass by reference와 ref, out, in 키워드  (1) 2021.02.21
C#의 Pass by value  (0) 2021.02.14
C# Value Type과 Reference Type  (1) 2021.02.12
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함