다음을 통해 공유


자습서: 첫 번째 분석기 및 코드 수정 작성

.NET 컴파일러 플랫폼 SDK는 C# 또는 Visual Basic 코드를 대상으로 하는 사용자 지정 진단(분석기), 코드 수정, 코드 리팩터링 및 진단 억제기를 만드는 데 필요한 도구를 제공합니다. 분석기는 규칙 위반을 인식하는 코드를 포함합니다. 코드 수정에는 위반을 해결하는 코드가 포함되어 있습니다. 구현하는 규칙은 코드 구조에서 코딩 스타일, 명명 규칙 등에 이르기까지 무엇이든 될 수 있습니다. .NET 컴파일러 플랫폼은 개발자가 코드를 작성할 때 분석을 실행하기 위한 프레임워크를 제공하며, 편집기에서 물결선 표시, Visual Studio 오류 목록 채우기, "전구" 제안 만들기, 제안된 수정 사항의 풍부한 미리 보기 표시 등 코드를 수정하기 위한 모든 Visual Studio UI 기능을 제공합니다.

이 자습서에서는 Roslyn API를 사용하여 분석기 만들기 및 함께 제공되는 코드 수정을 살펴봅니다. 분석기는 소스 코드 분석을 수행하고 사용자에게 문제를 보고하는 방법입니다. 필요에 따라 코드 수정을 분석기와 연결하여 사용자의 소스 코드 수정을 나타낼 수 있습니다. 이 자습서는 const 수정자를 사용하여 선언될 수 있는 지역 변수 선언이 아직 사용되지 않은 경우를 찾는 분석기를 만듭니다. 함께 제공되는 코드 수정은 해당 선언을 수정하여 한정자를 const 추가합니다.

필수 조건

Visual Studio 설치 관리자를 통해 .NET 컴파일러 플랫폼 SDK 를 설치해야 합니다.

설치 방법 - Visual Studio 설치 프로그램

Visual Studio 설치관리자에서 .NET 컴파일러 플랫폼 SDK를 찾는 방법에는 두 가지가 있습니다.

Visual Studio 설치 관리자를 사용하여 설치 - 워크로드 보기

.NET 컴파일러 플랫폼 SDK는 Visual Studio 확장 개발 워크로드의 일부로 자동으로 선택되지 않습니다. 선택적 구성 요소로 선택해야 합니다.

  1. Visual Studio 설치 관리자 실행
  2. 수정을 선택합니다.
  3. Visual Studio 확장 개발 워크로드를 확인합니다.
  4. 요약 트리에서 Visual Studio 확장 개발 노드를 엽니다.
  5. .NET 컴파일러 플랫폼 SDK에 대한 확인란을 선택합니다. 선택적 구성 요소 아래에서 마지막으로 찾을 수 있습니다.

필요에 따라 DGML 편집 기에서 시각화 도우미에 그래프를 표시할 수도 있습니다.

  1. 요약 트리에서 개별 구성 요소 노드를 엽니다.
  2. DGML 편집기 상자의 확인란을 선택합니다.

Visual Studio 설치 관리자를 사용하여 설치 - 개별 구성 요소 탭

  1. Visual Studio 설치 관리자 실행
  2. 수정을 선택합니다.
  3. 개별 구성 요소 탭 선택
  4. .NET 컴파일러 플랫폼 SDK에 대한 확인란을 선택합니다. 컴파일러, 빌드 도구 및 런타임 섹션 아래에 맨 위에 있습니다.

필요에 따라 DGML 편집 기에서 시각화 도우미에 그래프를 표시할 수도 있습니다.

  1. DGML 편집기의 확인란을 체크합니다. 코드 도구 섹션에서 찾을 수 있습니다.

분석기를 만들고 유효성을 검사하는 몇 가지 단계가 있습니다.

  1. 솔루션을 만듭니다.
  2. 분석기 이름 및 설명을 등록합니다.
  3. 분석기 경고 및 권장 사항을 보고합니다.
  4. 권장 사항을 수락하는 코드 수정을 구현합니다.
  5. 단위 테스트를 통해 분석을 개선합니다.

솔루션 만들기

  • Visual Studio에서 새 > 프로젝트 파일>...을 선택하여 새 프로젝트 대화 상자를 표시합니다.
  • Visual C# > 확장성 아래에서 코드 수정(.NET 표준)이 있는 분석기를 선택합니다.
  • 프로젝트 이름을 "MakeConst"로 지정하고 확인을 클릭합니다.

비고

컴파일 오류가 발생할 수 있습니다(MSB4062: "CompareBuildTaskVersion" 작업을 로드할 수 없음). 이 문제를 해결하려면 솔루션의 NuGet 패키지를 NuGet 패키지 관리자로 업데이트하거나 패키지 관리자 콘솔 창에서 사용합니다 Update-Package .

분석기 템플릿 살펴보기

코드 수정 템플릿이 있는 분석기는 다음 5개의 프로젝트를 만듭니다.

  • 분석기를 포함하는 MakeConst입니다.
  • 코드 수정이 포함된 MakeConst.CodeFixes입니다.
  • MakeConst.Package는 분석기 및 코드 수정을 위한 NuGet 패키지를 생성하는 데 사용됩니다.
  • 단위 테스트 프로젝트인 MakeConst.Test입니다.
  • MakeConst.Vsix는 새 분석기를 로드한 Visual Studio의 두 번째 인스턴스를 시작하는 기본 시작 프로젝트입니다. F5 키를 눌러 VSIX 프로젝트를 시작합니다.

비고

분석기는 .NET Core 환경(명령줄 빌드) 및 .NET Framework 환경(Visual Studio)에서 실행할 수 있으므로 .NET Standard 2.0을 대상으로 해야 합니다.

팁 (조언)

분석기를 실행하면 Visual Studio의 두 번째 복사본을 시작합니다. 이 두 번째 복사본은 다른 레지스트리 하이브를 사용하여 설정을 저장합니다. 이렇게 하면 Visual Studio의 두 복사본에서 시각적 설정을 구분할 수 있습니다. Visual Studio의 실험적 실행에 대해 다른 테마를 선택할 수 있습니다. 또한 Visual Studio의 실험적 실행을 사용하여 설정을 로밍하거나 Visual Studio 계정에 로그인하지 마세요. 설정을 다르게 유지합니다.

Hive에는 현재 개발 중인 분석기뿐만 아니라 이전에 열었던 모든 분석기도 포함됩니다. Roslyn hive를 다시 설정하려면 %LocalAppData%\Microsoft\VisualStudio에서 수동으로 삭제해야 합니다. Roslyn hive의 폴더 이름은 다음과 같이 16.0_9ae182f9Roslyn끝납니다Roslyn. 하이브를 삭제한 후 솔루션을 정리하고 다시 빌드해야 할 수 있습니다.

방금 시작한 두 번째 Visual Studio 인스턴스에서 새 C# 콘솔 애플리케이션 프로젝트를 만듭니다(모든 대상 프레임워크가 작동합니다. 분석기는 원본 수준에서 작동합니다.) 물결선 밑줄이 있는 토큰을 마우스로 가리키면 분석기에서 제공하는 경고 텍스트가 나타납니다.

템플릿은 다음 그림과 같이 형식 이름에 소문자가 포함된 각 형식 선언에 대한 경고를 보고하는 분석기를 만듭니다.

분석기 보고 경고

또한 템플릿은 소문자를 포함하는 모든 형식 이름을 모든 대문자로 변경하는 코드 수정을 제공합니다. 경고와 함께 표시된 전구를 클릭하여 제안된 변경 내용을 볼 수 있습니다. 제안된 변경 내용을 적용하면 솔루션의 형식 이름과 해당 형식에 대한 모든 참조가 업데이트됩니다. 이제 초기 분석기가 작동하는 것을 확인했으므로 두 번째 Visual Studio 인스턴스를 닫고 분석기 프로젝트로 돌아갑니다.

Visual Studio의 두 번째 복사본을 시작하고 분석기의 모든 변경 내용을 테스트하는 새 코드를 만들 필요가 없습니다. 또한 템플릿은 단위 테스트 프로젝트를 만듭니다. 이 프로젝트에는 두 가지 테스트가 포함되어 있습니다. TestMethod1 는 진단을 트리거하지 않고 코드를 분석하는 테스트의 일반적인 형식을 보여 줍니다. TestMethod2 는 진단을 트리거한 다음 제안된 코드 수정을 적용하는 테스트 형식을 보여 줍니다. 분석기 및 코드 수정을 빌드할 때 다양한 코드 구조에 대한 테스트를 작성하여 작업을 확인합니다. 분석기의 단위 테스트는 Visual Studio를 사용하여 대화형으로 테스트하는 것보다 훨씬 빠릅니다.

팁 (조언)

분석기 단위 테스트는 분석기를 트리거하지 않아야 하는 코드 구문을 알고 있을 때 유용한 도구입니다. Visual Studio의 다른 복사본에 분석기를 로드하는 것은 아직 생각하지 못한 구문을 탐색하고 찾는 데 유용한 도구입니다.

이 자습서에서는 로컬 상수로 변환할 수 있는 모든 지역 변수 선언을 사용자에게 보고하는 분석기를 작성합니다. 예를 들어 다음 코드를 고려합니다.

int x = 0;
Console.WriteLine(x);

위의 x 코드에서 상수 값이 할당되며 수정되지 않습니다. 한정자를 사용하여 const 선언할 수 있습니다.

const int x = 0;
Console.WriteLine(x);

변수를 상수로 만들 수 있는지 여부를 결정하는 분석은 구문 분석, 이니셜라이저 식의 상수 분석 및 데이터 흐름 분석이 필요하므로 변수가 기록되지 않도록 합니다. .NET 컴파일러 플랫폼은 이 분석을 보다 쉽게 수행할 수 있는 API를 제공합니다.

분석기 등록 만들기

템플릿은 MakeConstAnalyzer.cs 파일에 초기 DiagnosticAnalyzer 클래스를 만듭니다. 이 초기 분석기는 모든 분석기의 두 가지 중요한 속성을 보여줍니다.

  • 모든 진단 분석기는 작동하는 언어를 설명하는 특성을 제공해야 [DiagnosticAnalyzer] 합니다.
  • 모든 진단 분석기는 DiagnosticAnalyzer 클래스에서 직접 또는 간접적으로 파생되어야 합니다.

템플릿에는 분석기의 일부인 기본 기능도 표시됩니다.

  1. 작업을 등록합니다. 이 작업은 분석기가 코드 위반을 점검하도록 트리거해야 하는 코드 변경 사항을 나타냅니다. Visual Studio에서 등록된 작업과 일치하는 코드 편집을 검색하면 분석기 등록 메서드를 호출합니다.
  2. 진단을 만듭니다. 분석기가 위반을 감지하면 Visual Studio에서 사용자에게 위반 사실을 알리는 데 사용하는 진단 개체를 만듭니다.

DiagnosticAnalyzer.Initialize(AnalysisContext) 메서드를 재정의할 때 작업을 등록합니다. 이 자습서에서는 로컬 선언 을 찾는 구문 노드 를 방문하여 상수 값이 있는 구문 노드를 확인합니다. 선언이 일정할 수 있으면 분석기가 진단을 만들고 보고합니다.

첫 번째 단계는 등록 상수 및 Initialize 메서드를 업데이트하여 이러한 상수가 "Const 만들기" 분석기를 나타내도록 하는 것입니다. 대부분의 문자열 상수는 문자열 리소스 파일에 정의됩니다. 보다 쉽게 지역화하려면 이 연습을 따라야 합니다. MakeConst 분석기 프로젝트에 대한 Resources.resx 파일을 엽니다. 그러면 리소스 편집기가 표시됩니다. 다음과 같이 문자열 리소스를 업데이트합니다.

  • "AnalyzerDescription"을 "Variables that are not modified should be made constants."로 변경합니다.
  • AnalyzerMessageFormat을 "Variable '{0}' can be made constant"로 변경하십시오.
  • "AnalyzerTitle"을 "Variable can be made constant"로 변경합니다.

완료되면 다음 그림과 같이 리소스 편집기가 표시됩니다.

문자열 리소스 업데이트

나머지 변경 내용은 분석기 파일에 있습니다. Visual Studio에서 MakeConstAnalyzer.cs 엽니다. 기호에 대해 작동하는 작업에서 구문에서 작동하는 동작으로 등록된 작업을 변경합니다. MakeConstAnalyzerAnalyzer.Initialize 메서드에서 기호에 대한 작업을 등록하는 줄을 찾습니다.

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

다음 줄로 바꿉다.

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

이 변경 후에는 메서드를 삭제할 AnalyzeSymbol 수 있습니다. 이 분석기는 SyntaxKind.LocalDeclarationStatement를 검사하며 SymbolKind.NamedType 문장은 분석하지 않습니다. 아래에 빨간색 밑줄이 표시되어 있는 것을 확인하세요. 방금 추가한 AnalyzeNode 코드는 선언되지 않은 메서드를 참조합니다. 다음 코드를 사용하여 해당 메서드를 선언합니다.

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Category 다음 코드와 같이 MakeConstAnalyzer.cs "Usage"로 변경합니다.

private const string Category = "Usage";

const가 될 수 있는 로컬 선언 찾기

메서드의 첫 번째 버전을 작성해야 합니다 AnalyzeNode . 단일 로컬 선언을 찾아야 하며, const 되는 것은 아니지만 다음 코드와 같은 것이 될 수 있습니다.

int x = 0;
Console.WriteLine(x);

첫 번째 단계는 로컬 선언을 찾는 것입니다. MakeConstAnalyzer.cs 다음 코드를 AnalyzeNode 추가합니다.

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

분석기가 로컬 선언에 대한 변경 내용을 등록하고 로컬 선언만 등록했기 때문에 이 캐스트는 항상 성공합니다. 다른 노드 형식은 AnalyzeNode 메서드를 호출하지 않습니다. 다음으로 선언에서 const 수정자를 확인합니다. 찾으면 즉시 반환합니다. 다음 코드는 로컬 선언에서 모든 const 한정자를 찾습니다.

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

마지막으로 변수가 될 수 있는지 확인해야 합니다 const. 즉, 초기화된 후에 할당되지 않도록 합니다.

사용하는 SyntaxNodeAnalysisContext를 통해 몇 가지 의미 분석을 수행합니다. 인수를 context 사용하여 지역 변수 선언을 만들 const수 있는지 여부를 확인합니다. A Microsoft.CodeAnalysis.SemanticModel 는 단일 소스 파일의 모든 의미 체계 정보를 나타냅니다. 의미 체계 모델을 다루는 문서에서 자세히 알아볼 수 있습니다. 로컬 선언문에서 Microsoft.CodeAnalysis.SemanticModel 데이터 흐름 분석을 수행하는 데 사용합니다. 그런 다음 이 데이터 흐름 분석의 결과를 사용하여 지역 변수가 다른 곳에서는 새 값으로 작성되지 않도록 합니다. 확장 메서드를 GetDeclaredSymbol 호출하여 변수를 ILocalSymbol 검색하고 데이터 흐름 분석 컬렉션에 DataFlowAnalysis.WrittenOutside 포함되어 있지 않은지 확인합니다. 메서드의 끝에 다음 코드를 추가합니다 AnalyzeNode .

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

방금 추가된 코드는 변수가 수정되지 않도록 보장하므로 이를 통해 변수를 const로 만들 수 있습니다. 진단을 올려야 할 때입니다. 다음 코드를 마지막 줄로 추가합니다.AnalyzeNode

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

F5 키를 눌러 분석기를 실행하여 진행률을 확인할 수 있습니다. 이전에 만든 콘솔 애플리케이션을 로드한 다음, 다음 테스트 코드를 추가할 수 있습니다.

int x = 0;
Console.WriteLine(x);

전구가 나타나고 분석기가 진단을 보고해야 합니다. 그러나 Visual Studio 버전에 따라 다음 중 하나가 표시됩니다.

  • 템플릿에서 생성된 코드 수정을 여전히 사용하는 전구는 대문자로 만들 수 있다고 알려줍니다.
  • 편집기 맨 위에 'MakeConstCodeFixProvider'에 오류가 발생하여 사용하지 않도록 설정되었다는 배너 메시지가 표시됩니다. 이는 코드 수정 공급자가 아직 변경되지 않았기 때문에 여전히 TypeDeclarationSyntax 요소를 찾는 것으로 예상되며 LocalDeclarationStatementSyntax 요소를 기대하지 않기 때문입니다.

다음 섹션에서는 코드 수정을 작성하는 방법을 설명합니다.

수정 코드 작성

분석기는 하나 이상의 코드 수정을 제공할 수 있습니다. 코드 수정은 보고된 문제를 해결하는 편집을 정의합니다. 만든 분석기에서 const 키워드를 삽입하는 코드 수정을 제공할 수 있습니다.

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

사용자가 편집기에서 전구 UI에서 선택하고 Visual Studio에서 코드를 변경합니다.

CodeFixResources.resx 파일을 열고 "Make constant"로 변경 CodeFixTitle 합니다.

템플릿에서 추가한 MakeConstCodeFixProvider.cs 파일을 엽니다. 이 코드 수정은 진단 분석기에서 생성된 진단 ID에 이미 연결되었지만 올바른 코드 변환을 아직 구현하지는 않습니다.

다음으로, 메서드를 삭제합니다 MakeUppercaseAsync . 더 이상 적용되지 않습니다.

모든 코드 수정 공급자는 CodeFixProvider에서 파생됩니다. 모두 CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext)을(를) 오버라이드하여 사용 가능한 코드 수정 사항을 보고합니다. RegisterCodeFixesAsync에서 검색하려는 조상 노드 유형을 진단과 일치하도록 LocalDeclarationStatementSyntax로 변경합니다.

var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();

다음으로 마지막 줄을 변경하여 코드 수정을 등록합니다. 수정한 결과로, 기존 선언에 한정자 const를 추가하여 새 문서가 만들어지게 됩니다.

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
    CodeAction.Create(
        title: CodeFixResources.CodeFixTitle,
        createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
        equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
    diagnostic);

기호 MakeConstAsync에 방금 추가한 코드에서 빨간색 물결선이 표시됩니다. 다음과 같은 코드에 대한 MakeConstAsync 선언을 추가합니다.

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

MakeConstAsync 메서드는 사용자의 원본 파일을 나타내는 Documentconst 선언이 포함된 새로운 Document로 변환합니다.

선언문 앞에 삽입할 새 const 키워드 토큰을 만듭니다. 먼저 선언문의 첫 번째 토큰에서 불필요한 요소를 제거하고, 그것을 const 토큰에 연결해야 합니다. MakeConstAsync 메서드에 다음 코드를 추가합니다.

// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
    firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

다음으로, 다음 코드를 사용하여 선언에 토큰을 추가 const 합니다.

// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
    .WithModifiers(newModifiers)
    .WithDeclaration(localDeclaration.Declaration);

다음으로, C# 서식 규칙과 일치하도록 새 선언의 서식을 지정합니다. 기존 코드와 일치하도록 변경 내용의 서식을 지정하면 더 나은 환경을 만들 수 있습니다. 기존 코드 바로 다음에 다음 문을 추가합니다.

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

이 코드에는 새 네임스페이스가 필요합니다. 파일 맨 위에 다음 using 지시문을 추가합니다.

using Microsoft.CodeAnalysis.Formatting;

마지막 단계는 편집하는 것입니다. 이 프로세스에는 다음 세 가지 단계가 있습니다.

  1. 기존 문서에 대한 핸들을 가져옵니다.
  2. 기존 선언을 새 선언으로 바꿔서 새 문서를 만듭니다.
  3. 새 문서를 반환합니다.

메서드의 끝에 다음 코드를 추가합니다 MakeConstAsync .

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

코드 수정을 시도할 준비가 완료되었습니다. F5 키를 눌러 Visual Studio의 두 번째 인스턴스에서 분석기 프로젝트를 실행합니다. 두 번째 Visual Studio 인스턴스에서 새 C# 콘솔 애플리케이션 프로젝트를 만들고 상수 값으로 초기화된 몇 가지 지역 변수 선언을 Main 메서드에 추가합니다. 아래와 같이 경고로 보고됩니다.

const 경고를 만들 수 있습니다.

당신은 많은 진전을 이루었습니다. 선언 아래에는 만들 const수 있는 물결선이 있습니다. 그러나 아직 해야 할 일이 있습니다. consti으로 시작하는 선언에 추가한 다음, j를 추가하고 마지막으로 k까지 추가하면 제대로 작동합니다. 그러나 한정자를 k부터 다른 순서로 추가하면, 분석기에서 오류가 발생합니다: ij가 둘 다 이미 const 상태여야 constk으로 선언할 수 있습니다. 변수를 선언하고 초기화할 수 있는 다양한 방법을 처리하기 위해 더 많은 분석을 수행해야 합니다.

단위 테스트 작성하기

분석기 및 코드 수정은 const를 만들 수 있는 단일 선언의 간단한 사례에서 작동합니다. 이 구현에서 실수를 저지르는 수많은 가능한 선언문이 있습니다. 템플릿에서 작성한 단위 테스트 라이브러리를 사용하여 이러한 사례를 해결합니다. Visual Studio의 두 번째 복사본을 반복적으로 여는 것보다 훨씬 빠릅니다.

단위 테스트 프로젝트에서 MakeConstUnitTests.cs 파일을 엽니다. 템플릿은 분석기 및 코드 수정 단위 테스트에 대한 두 가지 일반적인 패턴을 따르는 두 개의 테스트를 만들었습니다. TestMethod1 는 분석기가 진단을 보고하지 않도록 하는 테스트 패턴을 보여 줍니다. TestMethod2 는 진단을 보고하고 코드 수정을 실행하는 패턴을 보여 줍니다.

템플릿은 단위 테스트에 Microsoft.CodeAnalysis.Testing 패키지를 사용합니다.

팁 (조언)

테스트 라이브러리는 다음을 비롯한 특수 태그 구문을 지원합니다.

  • [|text|]: 에 대한 text진단이 보고됨을 나타냅니다. 기본적으로 이 양식은 DiagnosticAnalyzer.SupportedDiagnostics에서 제공하는 정확히 하나의 DiagnosticDescriptor를 사용하여 분석기를 테스트하는 데만 사용할 수 있습니다.
  • {|ExpectedDiagnosticId:text|}: 진단 IdExpectedDiagnosticIdtext에 대해 보고됨을 나타냅니다.

클래스의 템플릿 테스트를 다음 테스트 메서드로 바꿉니다 MakeConstUnitTest .

        [TestMethod]
        public async Task LocalIntCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|int i = 0;|]
        Console.WriteLine(i);
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }

이 테스트를 실행하여 통과했는지 확인합니다. Visual Studio에서 테스트>Windows>테스트 탐색기를 선택하여 테스트 탐색기를 엽니다. 그런 다음 , 모두 실행을 선택합니다.

유효한 선언에 대한 테스트 만들기

일반적으로 분석기는 최소한의 작업을 수행하면서 가능한 한 빨리 종료해야 합니다. Visual Studio는 사용자가 코드를 편집할 때 등록된 분석기를 호출합니다. 응답성은 핵심 요구 사항입니다. 진단을 발생시키지 않아야 하는 코드에 대한 몇 가지 테스트 사례가 있습니다. 분석기는 이미 여러 테스트를 처리합니다. 다음 테스트 메서드를 추가하여 이러한 사례를 나타냅니다.

        [TestMethod]
        public async Task VariableIsAssigned_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }
        [TestMethod]
        public async Task VariableIsAlreadyConst_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }
        [TestMethod]
        public async Task NoInitializer_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i;
        i = 0;
        Console.WriteLine(i);
    }
}
");
        }

분석기가 이미 다음 조건을 처리하므로 이러한 테스트가 통과합니다.

  • 초기화 후 할당된 변수는 데이터 흐름 분석에 의해 검색됩니다.
  • const 키워드를 확인하여 이미 const 필터링된 선언이 삭제됩니다.
  • 이니셜라이저가 없는 선언은 선언 외부의 할당을 검색하는 데이터 흐름 분석에 의해 처리됩니다.

다음으로, 아직 처리하지 않은 조건에 대한 테스트 메서드를 추가합니다.

  • 이니셜라이저가 상수가 아닌 선언은 컴파일 시간 상수일 수 없기 때문입니다.

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

C#에서는 여러 선언을 하나의 문으로 허용하므로 훨씬 더 복잡할 수 있습니다. 다음 테스트 사례 문자열 상수는 다음과 같습니다.

        [TestMethod]
        public async Task MultipleInitializers_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0, j = DateTime.Now.DayOfYear;
        Console.WriteLine(i);
        Console.WriteLine(j);
    }
}
");
        }

변수 i 는 상수로 만들 수 있지만 변수 j 는 사용할 수 없습니다. 따라서 이 구문은 const 선언으로 만들 수 없습니다.

테스트를 다시 실행하면 마지막 두 테스트 사례가 실패하는 것을 볼 수 있습니다.

올바른 선언을 무시하도록 분석기 업데이트

이러한 조건과 일치하는 코드를 필터링하려면 분석기 메서드에 AnalyzeNode 몇 가지 향상된 기능이 필요합니다. 모두 관련된 조건이므로 유사한 변경으로 인해 이러한 모든 조건이 수정됩니다. AnalyzeNode에 다음 변경 내용을 적용합니다.

  • 의미 체계 분석은 단일 변수 선언을 검사했습니다. 이 코드는 동일한 문에 foreach 선언된 모든 변수를 검사하는 루프에 있어야 합니다.
  • 선언된 각 변수에는 이니셜라이저가 있어야 합니다.
  • 선언된 각 변수의 이니셜라이저는 컴파일 시간 상수여야 합니다.

AnalyzeNode 메서드에서 원래 의미 체계 분석을 바꿉니다.

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

다음 코드 조각을 사용하여

// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    EqualsValueClauseSyntax initializer = variable.Initializer;
    if (initializer == null)
    {
        return;
    }

    Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
    if (!constantValue.HasValue)
    {
        return;
    }
}

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    // Retrieve the local symbol for each variable in the local declaration
    // and ensure that it is not written outside of the data flow analysis region.
    ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
    {
        return;
    }
}

첫 번째 foreach 루프는 구문 분석을 사용하여 각 변수 선언을 검사합니다. 첫 번째 검사는 변수에 이니셜라이저가 있는지 확인합니다. 두 번째 검사는 이니셜라이저가 상수임을 보장합니다. 두 번째 루프에는 원래 의미 체계 분석이 있습니다. 의미 체계 검사는 성능에 더 큰 영향을 주므로 별도의 루프에 있습니다. 테스트를 다시 실행하면 모두 통과할 것입니다.

최종 다듬기 추가

거의 완료되었습니다. 분석기에서 처리해야 할 몇 가지 조건이 더 있습니다. 사용자가 코드를 작성하는 동안 Visual Studio에서 분석기를 호출합니다. 컴파일되지 않는 코드에 대해 분석기가 호출되는 경우가 많습니다. 진단 분석기 AnalyzeNode 메서드는 상수 값을 변수 형식으로 변환할 수 있는지 확인하지 않습니다. 따라서 현재 구현에서는 int i = "abc"와 같은 잘못된 선언을 아무 문제 없이 로컬 상수로 변환합니다. 이 사례에 대한 테스트 메서드를 추가합니다.

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

또한 참조 형식이 제대로 처리되지 않습니다. 참조 형식에서 유일하게 허용되는 상수 값은 null입니다. 단, System.String는 문자열 리터럴을 허용합니다. 즉, const string s = "abc" 합법적이지만 const object s = "abc" 그렇지 않습니다. 이 코드 조각은 해당 조건을 확인합니다.

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

철저하게 하려면 문자열에 대한 상수 선언을 만들 수 있도록 다른 테스트를 추가해야 합니다. 다음 코드 조각은 진단을 발생시키는 코드와 수정이 적용된 후의 코드를 모두 정의합니다.

        [TestMethod]
        public async Task StringCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|string s = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string s = ""abc"";
    }
}
");
        }

마지막으로, 변수가 키워드로 var 선언되면 코드 수정에서 잘못된 작업을 수행하고 C# 언어에서 지원되지 않는 선언을 생성합니다 const var . 이 버그를 해결하려면 코드 수정에서 키워드를 var 유추된 형식의 이름으로 바꿔야 합니다.

        [TestMethod]
        public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = 4;|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int item = 4;
    }
}
");
        }

        [TestMethod]
        public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string item = ""abc"";
    }
}
");
        }

다행히 위의 모든 버그는 방금 배운 것과 동일한 기술을 사용하여 해결할 수 있습니다.

첫 번째 버그를 해결하려면 먼저 MakeConstAnalyzer.cs 열고 각 로컬 선언의 이니셜라이저를 검사하여 상수 값이 할당되었는지 확인하는 foreach 루프를 찾습니다. 첫 번째 foreach 루프 바로 전에 호출 context.SemanticModel.GetTypeInfo() 하여 선언된 로컬 선언 형식에 대한 자세한 정보를 검색합니다.

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

그런 다음 루프 내에서 foreach 각 이니셜라이저를 확인하여 변수 형식으로 변환할 수 있는지 확인합니다. 이니셜라이저가 상수인지 확인한 후 다음 검사를 추가합니다.

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
    return;
}

다음 변경 내용은 마지막 변경 내용에 따라 빌드됩니다. 첫 번째 foreach 루프의 닫는 중괄호 앞에 다음 코드를 추가하여 상수가 문자열 또는 null일 때 로컬 선언의 형식을 확인합니다.

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

키워드를 올바른 형식 이름으로 바꾸려면 코드 수정 공급자에 var 코드를 좀 더 작성해야 합니다. MakeConstCodeFixProvider.cs로 돌아갑니다. 추가할 코드는 다음 단계를 수행합니다.

  • 선언이 var 선언인지 확인하십시오.
  • 유추된 형식에 대한 새 형식을 만듭니다.
  • 형식 선언이 별칭이 아닌지 확인합니다. 그렇다면 선언 const var하는 것이 합법적입니다.
  • 이 프로그램에서 var가 타입 이름이 아닌지 확인합니다. (그렇다면, const var 합법적입니다).
  • 전체 형식 이름 간소화

그것은 많은 코드처럼 들립니다. 그것은 아니야. 선언하고 초기화하는 줄을 다음 코드로 바꿉니다 newLocal . 초기화 직후 바로 발생합니다 newModifiers.

// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
    SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

    // Special case: Ensure that 'var' isn't actually an alias to another type
    // (e.g. using var = System.String).
    IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
    if (aliasInfo == null)
    {
        // Retrieve the type inferred for var.
        ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

        // Special case: Ensure that 'var' isn't actually a type named 'var'.
        if (type.Name != "var")
        {
            // Create a new TypeSyntax for the inferred type. Be careful
            // to keep any leading and trailing trivia from the var keyword.
            TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
                .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
                .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

            // Add an annotation to simplify the type name.
            TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

            // Replace the type in the variable declaration.
            variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
        }
    }
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
                           .WithDeclaration(variableDeclaration);

특정 Simplifier 유형을 사용하려면,using 지시어를 하나 추가해야 합니다.

using Microsoft.CodeAnalysis.Simplification;

테스트를 실행하면 모두 통과해야 합니다. 완료한 분석기를 실행하며 스스로를 축하해 보세요. Ctrl+F5 키를 눌러 Roslyn 미리 보기 확장이 로드된 Visual Studio의 두 번째 인스턴스에서 분석기 프로젝트를 실행합니다.

  • 두 번째 Visual Studio 인스턴스에서 새 C# 콘솔 애플리케이션 프로젝트를 만들고 Main 메서드에 추가 int x = "abc"; 합니다. 첫 번째 버그 수정 덕분에 이 지역 변수 선언에 대해 경고가 보고되지 않아야 합니다(예상대로 컴파일러 오류가 발생하지만).
  • 다음으로 Main 메서드에 추가 object s = "abc"; 합니다. 두 번째 버그 수정으로 인해 경고를 보고하지 않아야 합니다.
  • 마지막으로 키워드를 사용하는 다른 지역 변수를 추가합니다 var . 경고가 보고되고 왼쪽 아래에 제안이 나타납니다.
  • 편집기 캐리트를 물결선 밑줄 위로 이동하고 Ctrl 키를+ 누릅니다. 제안된 코드 수정을 표시합니다. 코드 수정을 선택하면 이제 키워드가 var 올바르게 처리됩니다.

마지막으로 다음 코드를 추가합니다.

int i = 2;
int j = 32;
int k = i + j;

이러한 변경 후에는 처음 두 변수에서만 빨간색 물결선이 표시됩니다. constij에 추가하면 k에 새 경고가 발생할 수 있습니다, 왜냐하면 이제 const가 될 수 있기 때문입니다.

축하합니다! 문제를 감지하기 위해 즉석 코드 분석을 수행하고 이를 해결하기 위한 빠른 수정을 제공하는 첫 번째 .NET 컴파일러 플랫폼 확장을 만들었습니다. 그 과정에서 .NET 컴파일러 플랫폼 SDK(Roslyn API)의 일부인 많은 코드 API를 배웠습니다. 샘플 GitHub 리포지토리에서 완료된 샘플 에 대해 작업을 확인할 수 있습니다.

기타 리소스