This is an automated email from the ASF dual-hosted git repository. nightowl888 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/lucenenet.git
commit 05577aaad27b2ff2f2333e058a86e8f49014b50e Author: Shad Storhaug <[email protected]> AuthorDate: Mon Feb 3 18:38:59 2020 +0700 Added Lucene.Net.CodeAnalysis project with Roslyn analyzers and code fixes in C#/VB to ensure TokenStream subclasses or their IncrementToken() method are marked sealed. (Fixes LUCENENET-642) --- Lucene.Net.sln | 14 ++ build/Dependencies.props | 3 + .../publish-test-results-for-test-projects.yml | 14 +- src/Lucene.Net/Lucene.Net.csproj | 15 ++ .../Lucene.Net.CodeAnalysis.csproj | 36 +++ ...00_SealIncrementTokenMethodCSCodeFixProvider.cs | 99 ++++++++ ...00_SealIncrementTokenMethodVBCodeFixProvider.cs | 97 ++++++++ ...ne1000_SealTokenStreamClassCSCodeFixProvider.cs | 71 ++++++ ...rItsIncrementTokenMethodMustBeSealedAnalyzer.cs | 125 ++++++++++ .../Lucene.Net.CodeAnalysis/tools/install.ps1 | 58 +++++ .../Lucene.Net.CodeAnalysis/tools/uninstall.ps1 | 65 +++++ .../Helpers/CodeFixVerifier.Helper.cs | 85 +++++++ .../Helpers/DiagnosticResult.cs | 87 +++++++ .../Helpers/DiagnosticVerifier.Helper.cs | 172 +++++++++++++ .../Lucene.Net.Tests.CodeAnalysis.csproj | 42 ++++ ...00_SealIncrementTokenMethodCSCodeFixProvider.cs | 91 +++++++ ...00_SealIncrementTokenMethodVBCodeFixProvider.cs | 91 +++++++ ...ne1000_SealTokenStreamClassCSCodeFixProvider.cs | 91 +++++++ .../Verifiers/CodeFixVerifier.cs | 128 ++++++++++ .../Verifiers/DiagnosticVerifier.cs | 271 +++++++++++++++++++++ 20 files changed, 1653 insertions(+), 2 deletions(-) diff --git a/Lucene.Net.sln b/Lucene.Net.sln index 5b61c2b..54a2aa5 100644 --- a/Lucene.Net.sln +++ b/Lucene.Net.sln @@ -195,6 +195,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lucene.Net.Analysis.Morfolo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lucene.Net.Tests.Analysis.Morfologik", "src\Lucene.Net.Tests.Analysis.Morfologik\Lucene.Net.Tests.Analysis.Morfologik.csproj", "{435F91AD-8BA4-4376-904C-385A165C1AF0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lucene.Net.CodeAnalysis", "src\dotnet\Lucene.Net.CodeAnalysis\Lucene.Net.CodeAnalysis.csproj", "{A9A5C2DC-C4EA-49B4-805A-6CEB5D246D2F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lucene.Net.Tests.CodeAnalysis", "src\dotnet\Lucene.Net.Tests.CodeAnalysis\Lucene.Net.Tests.CodeAnalysis.csproj", "{158F5D30-8B96-4C49-9009-0B8ACEDF8546}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -445,6 +449,14 @@ Global {435F91AD-8BA4-4376-904C-385A165C1AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {435F91AD-8BA4-4376-904C-385A165C1AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {435F91AD-8BA4-4376-904C-385A165C1AF0}.Release|Any CPU.Build.0 = Release|Any CPU + {A9A5C2DC-C4EA-49B4-805A-6CEB5D246D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9A5C2DC-C4EA-49B4-805A-6CEB5D246D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9A5C2DC-C4EA-49B4-805A-6CEB5D246D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9A5C2DC-C4EA-49B4-805A-6CEB5D246D2F}.Release|Any CPU.Build.0 = Release|Any CPU + {158F5D30-8B96-4C49-9009-0B8ACEDF8546}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {158F5D30-8B96-4C49-9009-0B8ACEDF8546}.Debug|Any CPU.Build.0 = Debug|Any CPU + {158F5D30-8B96-4C49-9009-0B8ACEDF8546}.Release|Any CPU.ActiveCfg = Release|Any CPU + {158F5D30-8B96-4C49-9009-0B8ACEDF8546}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -457,6 +469,8 @@ Global {CF3A74CA-FEFD-4F41-961B-CC8CF8D96286} = {8CA61D33-3590-4024-A304-7B1F75B50653} {4B054831-5275-44E2-A4D4-CA0B19BEE19A} = {8CA61D33-3590-4024-A304-7B1F75B50653} {1F5574FE-19F7-4F10-9B88-76A938621F5B} = {4DF7EACE-2B25-43F6-B558-8520BF20BD76} + {A9A5C2DC-C4EA-49B4-805A-6CEB5D246D2F} = {8CA61D33-3590-4024-A304-7B1F75B50653} + {158F5D30-8B96-4C49-9009-0B8ACEDF8546} = {8CA61D33-3590-4024-A304-7B1F75B50653} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F2179CC-CFD2-4419-AB74-D72856931F36} diff --git a/build/Dependencies.props b/build/Dependencies.props index 1d1687a..05f2eac 100644 --- a/build/Dependencies.props +++ b/build/Dependencies.props @@ -41,6 +41,9 @@ <J2NPackageVersion>2.0.0-beta-0001</J2NPackageVersion> <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>1.0.3</MicrosoftAspNetCoreHttpAbstractionsPackageVersion> <MicrosoftAspNetCoreTestHostPackageVersion>1.0.3</MicrosoftAspNetCoreTestHostPackageVersion> + <MicrosoftCodeAnalysisAnalyzersPackageVersion>2.9.8</MicrosoftCodeAnalysisAnalyzersPackageVersion> + <MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>3.4.0</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion> + <MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion>3.4.0</MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion> <MicrosoftCSharpPackageVersion>4.4.0</MicrosoftCSharpPackageVersion> <MicrosoftExtensionsDependencyModelPackageVersion>2.0.0</MicrosoftExtensionsDependencyModelPackageVersion> <MicrosoftNETTestSdkPackageVersion>16.2.0</MicrosoftNETTestSdkPackageVersion> diff --git a/build/azure-templates/publish-test-results-for-test-projects.yml b/build/azure-templates/publish-test-results-for-test-projects.yml index f33aa53..1f1f3ef 100644 --- a/build/azure-templates/publish-test-results-for-test-projects.yml +++ b/build/azure-templates/publish-test-results-for-test-projects.yml @@ -71,7 +71,17 @@ steps: testResultsArtifactName: '${{ parameters.testResultsArtifactName }}' testResultsFileName: '${{ parameters.testResultsFileName }}' -# Special case: Only supports .netcoreapp3.0 +# Special case: Only supports .NET Standard 2.0 +- template: publish-test-results.yml + parameters: + framework: 'netcoreapp2.2' + testProjectName: 'Lucene.Net.Tests.CodeAnalysis' + osName: '${{ parameters.osName }}' + testResultsFormat: '${{ parameters.testResultsFormat }}' + testResultsArtifactName: '${{ parameters.testResultsArtifactName }}' + testResultsFileName: '${{ parameters.testResultsFileName }}' + +# Special case: Only supports .netcoreapp3.1 - template: publish-test-results.yml parameters: framework: 'netcoreapp3.1' @@ -81,7 +91,7 @@ steps: testResultsArtifactName: '${{ parameters.testResultsArtifactName }}' testResultsFileName: '${{ parameters.testResultsFileName }}' -# Special case: Only supports .net45 +# Special case: Only supports .net48 - template: publish-test-results.yml parameters: framework: 'net48' diff --git a/src/Lucene.Net/Lucene.Net.csproj b/src/Lucene.Net/Lucene.Net.csproj index edcf9bd..0b369ce 100644 --- a/src/Lucene.Net/Lucene.Net.csproj +++ b/src/Lucene.Net/Lucene.Net.csproj @@ -46,6 +46,21 @@ </ItemGroup> <ItemGroup> + <Compile Include="..\CommonAssemblyKeys.cs" Link="Properties\CommonAssemblyKeys.cs" /> + </ItemGroup> + + <PropertyGroup Label="NuGet Package File Paths"> + <LuceneNetCodeAnalysisDir>$(SolutionDir)src\dotnet\Lucene.Net.CodeAnalysis\</LuceneNetCodeAnalysisDir> + <LuceneNetCodeAnalysisAssemblyFile>$(LuceneNetCodeAnalysisDir)bin\$(Configuration)\netstandard2.0\*.dll</LuceneNetCodeAnalysisAssemblyFile> + </PropertyGroup> + + <ItemGroup Label="NuGet Package Files"> + <None Include="$(LuceneNetCodeAnalysisDir)tools\*.ps1" Pack="true" PackagePath="tools" /> + <None Include="$(LuceneNetCodeAnalysisAssemblyFile)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> + <None Include="$(LuceneNetCodeAnalysisAssemblyFile)" Pack="true" PackagePath="analyzers/dotnet/vb" Visible="false" /> + </ItemGroup> + + <ItemGroup> <PackageReference Include="J2N" Version="$(J2NPackageVersion)" /> </ItemGroup> diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/Lucene.Net.CodeAnalysis.csproj b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene.Net.CodeAnalysis.csproj new file mode 100644 index 0000000..628d6f6 --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene.Net.CodeAnalysis.csproj @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +--> +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <IncludeBuildOutput>false</IncludeBuildOutput> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="$(MicrosoftCodeAnalysisAnalyzersPackageVersion)" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion)" PrivateAssets="all" /> + <PackageReference Update="NETStandard.Library" PrivateAssets="all" /> + </ItemGroup> + +</Project> diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealIncrementTokenMethodCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealIncrementTokenMethodCSCodeFixProvider.cs new file mode 100644 index 0000000..a6e3569 --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealIncrementTokenMethodCSCodeFixProvider.cs @@ -0,0 +1,99 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Lucene1000_SealIncrementTokenMethodCSCodeFixProvider)), Shared] + public class Lucene1000_SealIncrementTokenMethodCSCodeFixProvider : CodeFixProvider + { + private const string Title = "Add sealed keyword to IncrementToken() method"; + + public sealed override ImmutableArray<string> FixableDiagnosticIds + { + get { return ImmutableArray.Create(Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.DiagnosticId); } + } + + public sealed override FixAllProvider GetFixAllProvider() + { + // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Find the type declaration identified by the diagnostic. + var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().First(); + + var incrementTokenMethodDeclaration = GetIncrementTokenMethodDeclaration(declaration); + + // If we can't find the method, we skip registration for this fix + if (incrementTokenMethodDeclaration != null) + { + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => AddSealedKeywordAsync(context.Document, incrementTokenMethodDeclaration, c), + equivalenceKey: Title), + diagnostic); + } + } + + private async Task<Document> AddSealedKeywordAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + + DeclarationModifiers modifiers = DeclarationModifiers.None; + if (methodDeclaration.Modifiers.Any(SyntaxKind.NewKeyword)) + { + modifiers |= DeclarationModifiers.New; + } + if (methodDeclaration.Modifiers.Any(SyntaxKind.OverrideKeyword)) + { + modifiers |= DeclarationModifiers.Override; + } + if (methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)) + { + modifiers |= DeclarationModifiers.Unsafe; + } + modifiers |= DeclarationModifiers.Sealed; + + var newMethodDeclaration = generator.WithModifiers(methodDeclaration, modifiers); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = oldRoot.ReplaceNode(methodDeclaration, newMethodDeclaration); + + // Return document with transformed tree. + return document.WithSyntaxRoot(newRoot); + } + + private MethodDeclarationSyntax GetIncrementTokenMethodDeclaration(ClassDeclarationSyntax classDeclaration) + { + foreach (var member in classDeclaration.Members.Where(m => m.Kind() == SyntaxKind.MethodDeclaration)) + { + var methodDeclaration = (MethodDeclarationSyntax)member; + + if (methodDeclaration.Identifier.ValueText == "IncrementToken") + { + return methodDeclaration; + } + } + return null; + } + } +} diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealIncrementTokenMethodVBCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealIncrementTokenMethodVBCodeFixProvider.cs new file mode 100644 index 0000000..9cc71a2 --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealIncrementTokenMethodVBCodeFixProvider.cs @@ -0,0 +1,97 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using Microsoft.CodeAnalysis.Editing; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis +{ + [ExportCodeFixProvider(LanguageNames.VisualBasic, Name = nameof(Lucene1000_SealIncrementTokenMethodVBCodeFixProvider)), Shared] + public class Lucene1000_SealIncrementTokenMethodVBCodeFixProvider : CodeFixProvider + { + private const string Title = "Add NotOverridable keyword to IncrementToken() method"; + + public sealed override ImmutableArray<string> FixableDiagnosticIds + { + get { return ImmutableArray.Create(Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.DiagnosticId); } + } + + public sealed override FixAllProvider GetFixAllProvider() + { + // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Find the type declaration identified by the diagnostic. + var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ClassBlockSyntax>().First(); + + var incrementTokenMethodDeclaration = GetIncrementTokenMethodDeclaration(declaration); + + // If we can't find the method, we skip registration for this fix + if (incrementTokenMethodDeclaration != null) + { + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => AddSealedKeywordAsync(context.Document, incrementTokenMethodDeclaration, c), + equivalenceKey: Title), + diagnostic); + } + } + + private async Task<Document> AddSealedKeywordAsync(Document document, MethodStatementSyntax methodDeclaration, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + + DeclarationModifiers modifiers = DeclarationModifiers.None; + if (methodDeclaration.Modifiers.Any(SyntaxKind.NewKeyword)) + { + modifiers |= DeclarationModifiers.New; + } + if (methodDeclaration.Modifiers.Any(SyntaxKind.OverridesKeyword)) + { + modifiers |= DeclarationModifiers.Override; + } + modifiers |= DeclarationModifiers.Sealed; + + var newMethodDeclaration = generator.WithModifiers(methodDeclaration, modifiers); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = oldRoot.ReplaceNode(methodDeclaration, newMethodDeclaration); + + // Return document with transformed tree. + return document.WithSyntaxRoot(newRoot); + } + + private MethodStatementSyntax GetIncrementTokenMethodDeclaration(ClassBlockSyntax classBlock) + { + foreach (var member in classBlock.Members.Where(m => m.IsKind(SyntaxKind.FunctionBlock))) + { + var functionBlock = (MethodBlockSyntax)member; + + var methodDeclaration = (MethodStatementSyntax)functionBlock.BlockStatement; + + if (methodDeclaration.Identifier.ValueText == "IncrementToken") + { + return methodDeclaration; + } + } + return null; + } + } +} diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealTokenStreamClassCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealTokenStreamClassCSCodeFixProvider.cs new file mode 100644 index 0000000..89c8e8a --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_SealTokenStreamClassCSCodeFixProvider.cs @@ -0,0 +1,71 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Lucene1000_SealTokenStreamClassCSCodeFixProvider)), Shared] + public class Lucene1000_SealTokenStreamClassCSCodeFixProvider : CodeFixProvider + { + private const string Title = "Add sealed keyword to class definition"; + + public sealed override ImmutableArray<string> FixableDiagnosticIds + { + get { return ImmutableArray.Create(Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.DiagnosticId); } + } + + public sealed override FixAllProvider GetFixAllProvider() + { + // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Find the type declaration identified by the diagnostic. + var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().First(); + + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => AddSealedKeywordAsync(context.Document, declaration, c), + equivalenceKey: Title), + diagnostic); + } + + private async Task<Document> AddSealedKeywordAsync(Document document, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + + DeclarationModifiers modifiers = DeclarationModifiers.None; + if (classDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + modifiers |= DeclarationModifiers.Partial; + } + modifiers |= DeclarationModifiers.Sealed; + + var newClassDeclaration = generator.WithModifiers(classDeclaration, modifiers); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = oldRoot.ReplaceNode(classDeclaration, newClassDeclaration); + + // Return document with transformed tree. + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.cs b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.cs new file mode 100644 index 0000000..8282fbf --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.cs @@ -0,0 +1,125 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Linq; + + +namespace Lucene.Net.CodeAnalysis +{ + // LUCENENET: In Lucene, the TokenStream class had an AssertFinal() method with Reflection code to determine + // whether subclasses or their IncrementToken() method were marked sealed. This code was not intended to be + // used at runtime. In .NET, debug code is compiled out, and running Reflection code conditionally is not + // practical. Instead, this analyzer is installed into the IDE and used at design/build time. + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public class Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "Lucene1000"; + private const string Category = "Design"; + + private const string TitleCS = "TokenStream derived type or its IncrementToken() method must be marked sealed."; + private const string MessageFormatCS = "Type name '{0}' or its IncrementToken() method must be marked sealed."; + private const string DescriptionCS = "TokenStream derived types or their IncrementToken() method must be marked sealed."; + + private const string TitleVB = "TokenStream derived type must be marked NotInheritable or its IncrementToken() method must be marked NotOverridable."; + private const string MessageFormatVB = "Type name '{0}' must be marked NotInheritable or its IncrementToken() method must be marked NotOverridable."; + private const string DescriptionVB = "TokenStream derived types must be marked NotInheritable or their IncrementToken() method must be marked NotOverridable."; + + private static readonly DiagnosticDescriptor RuleCS = new DiagnosticDescriptor(DiagnosticId, TitleCS, MessageFormatCS, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DescriptionCS); + + private static readonly DiagnosticDescriptor RuleVB = new DiagnosticDescriptor(DiagnosticId, TitleVB, MessageFormatVB, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DescriptionVB); + + public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(RuleCS, RuleVB); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNodeCS, Microsoft.CodeAnalysis.CSharp.SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeNodeVB, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.ClassBlock); + } + + private static void AnalyzeNodeCS(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax)context.Node; + + var classTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol; + + if (!InheritsFrom(classTypeSymbol, "Lucene.Net.Analysis.TokenStream")) + { + return; + } + if (classDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.CSharp.SyntaxKind.SealedKeyword) || classDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.CSharp.SyntaxKind.AbstractKeyword)) + { + return; + } + foreach (var member in classDeclaration.Members.Where(m => m.Kind() == Microsoft.CodeAnalysis.CSharp.SyntaxKind.MethodDeclaration)) + { + var methodDeclaration = (Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax)member; + + if (methodDeclaration.Identifier.ValueText == "IncrementToken") + { + if (methodDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.CSharp.SyntaxKind.SealedKeyword)) + return; // The method is marked sealed, check passed + else + break; // The method is not marked sealed, exit the loop and report + } + } + + context.ReportDiagnostic(Diagnostic.Create(RuleCS, context.Node.GetLocation(), classDeclaration.Identifier)); + } + + private static void AnalyzeNodeVB(SyntaxNodeAnalysisContext context) + { + var classBlock = (Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassBlockSyntax)context.Node; + + var classDeclaration = classBlock.ClassStatement; + + var classTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol; + + if (!InheritsFrom(classTypeSymbol, "Lucene.Net.Analysis.TokenStream")) + { + return; + } + if (classDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.NotInheritableKeyword) || classDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.MustInheritKeyword)) + { + return; + } + foreach (var member in classBlock.Members.Where(m => m.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.FunctionBlock))) + { + var functionBlock = (Microsoft.CodeAnalysis.VisualBasic.Syntax.MethodBlockSyntax)member; + + var methodDeclaration = (Microsoft.CodeAnalysis.VisualBasic.Syntax.MethodStatementSyntax)functionBlock.BlockStatement; + + if (methodDeclaration.Identifier.ValueText == "IncrementToken") + { + if (methodDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.NotOverridableKeyword)) + return; // The method is marked sealed, check passed + else + break; // The method is not marked sealed, exit the loop and report + } + } + + context.ReportDiagnostic(Diagnostic.Create(RuleVB, context.Node.GetLocation(), classDeclaration.Identifier)); + } + + private static bool InheritsFrom(ITypeSymbol symbol, string expectedParentTypeName) + { + while (true) + { + if (symbol.ToString().Equals(expectedParentTypeName)) + { + return true; + } + + if (symbol.BaseType != null) + { + symbol = symbol.BaseType; + continue; + } + break; + } + + return false; + } + } +} diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/tools/install.ps1 b/src/dotnet/Lucene.Net.CodeAnalysis/tools/install.ps1 new file mode 100644 index 0000000..c1c3d88 --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/tools/install.ps1 @@ -0,0 +1,58 @@ +param($installPath, $toolsPath, $package, $project) + +if($project.Object.SupportsPackageDependencyResolution) +{ + if($project.Object.SupportsPackageDependencyResolution()) + { + # Do not install analyzers via install.ps1, instead let the project system handle it. + return + } +} + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + if (Test-Path $analyzersPath) + { + # Install the language agnostic analyzers. + foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} + +# $project.Type gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Install language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} \ No newline at end of file diff --git a/src/dotnet/Lucene.Net.CodeAnalysis/tools/uninstall.ps1 b/src/dotnet/Lucene.Net.CodeAnalysis/tools/uninstall.ps1 new file mode 100644 index 0000000..65a8623 --- /dev/null +++ b/src/dotnet/Lucene.Net.CodeAnalysis/tools/uninstall.ps1 @@ -0,0 +1,65 @@ +param($installPath, $toolsPath, $package, $project) + +if($project.Object.SupportsPackageDependencyResolution) +{ + if($project.Object.SupportsPackageDependencyResolution()) + { + # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. + return + } +} + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall the language agnostic analyzers. + if (Test-Path $analyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + } + } +} + +# $project.Type gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) + { + if($project.Object.AnalyzerReferences) + { + try + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + catch + { + + } + } + } + } +} \ No newline at end of file diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/CodeFixVerifier.Helper.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/CodeFixVerifier.Helper.cs new file mode 100644 index 0000000..6d32048 --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/CodeFixVerifier.Helper.cs @@ -0,0 +1,85 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace TestHelper +{ + /// <summary> + /// Diagnostic Producer class with extra methods dealing with applying codefixes + /// All methods are static + /// </summary> + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// <summary> + /// Apply the inputted CodeAction to the inputted document. + /// Meant to be used to apply codefixes. + /// </summary> + /// <param name="document">The Document to apply the fix on</param> + /// <param name="codeAction">A CodeAction that will be applied to the Document.</param> + /// <returns>A Document with the changes from the CodeAction</returns> + private static Document ApplyFix(Document document, CodeAction codeAction) + { + var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; + var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution; + return solution.GetDocument(document.Id); + } + + /// <summary> + /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. + /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, + /// this method may not necessarily return the new one. + /// </summary> + /// <param name="diagnostics">The Diagnostics that existed in the code before the CodeFix was applied</param> + /// <param name="newDiagnostics">The Diagnostics that exist in the code after the CodeFix was applied</param> + /// <returns>A list of Diagnostics that only surfaced in the code after the CodeFix was applied</returns> + private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics) + { + var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + + int oldIndex = 0; + int newIndex = 0; + + while (newIndex < newArray.Length) + { + if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) + { + ++oldIndex; + ++newIndex; + } + else + { + yield return newArray[newIndex++]; + } + } + } + + /// <summary> + /// Get the existing compiler diagnostics on the inputted document. + /// </summary> + /// <param name="document">The Document to run the compiler diagnostic analyzers on</param> + /// <returns>The compiler diagnostics that were found in the code</returns> + private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document) + { + return document.GetSemanticModelAsync().Result.GetDiagnostics(); + } + + /// <summary> + /// Given a document, turn it into a string based on the syntax root + /// </summary> + /// <param name="document">The Document to be converted to a string</param> + /// <returns>A string containing the syntax of the Document after formatting</returns> + private static string GetStringFromDocument(Document document) + { + var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; + var root = simplifiedDoc.GetSyntaxRootAsync().Result; + root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); + return root.GetText().ToString(); + } + } +} + diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticResult.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticResult.cs new file mode 100644 index 0000000..dde80c4 --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticResult.cs @@ -0,0 +1,87 @@ +using Microsoft.CodeAnalysis; +using System; + +namespace TestHelper +{ + /// <summary> + /// Location where the diagnostic appears, as determined by path, line number, and column number. + /// </summary> + public struct DiagnosticResultLocation + { + public DiagnosticResultLocation(string path, int line, int column) + { + if (line < -1) + { + throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); + } + + if (column < -1) + { + throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); + } + + this.Path = path; + this.Line = line; + this.Column = column; + } + + public string Path { get; } + public int Line { get; } + public int Column { get; } + } + + /// <summary> + /// Struct that stores information about a Diagnostic appearing in a source + /// </summary> + public struct DiagnosticResult + { + private DiagnosticResultLocation[] locations; + + public DiagnosticResultLocation[] Locations + { + get + { + if (this.locations == null) + { + this.locations = new DiagnosticResultLocation[] { }; + } + return this.locations; + } + + set + { + this.locations = value; + } + } + + public DiagnosticSeverity Severity { get; set; } + + public string Id { get; set; } + + public string Message { get; set; } + + public string Path + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Path : ""; + } + } + + public int Line + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Line : -1; + } + } + + public int Column + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Column : -1; + } + } + } +} diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs new file mode 100644 index 0000000..69d8066 --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs @@ -0,0 +1,172 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace TestHelper +{ + /// <summary> + /// Class for turning strings into documents and getting the diagnostics on them + /// All methods are static + /// </summary> + public abstract partial class DiagnosticVerifier + { + private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); + private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); + private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); + private static readonly MetadataReference LuceneNetReference = MetadataReference.CreateFromFile(typeof(Lucene.Net.Analysis.Analyzer).Assembly.Location); + + internal static string DefaultFilePathPrefix = "Test"; + internal static string CSharpDefaultFileExt = "cs"; + internal static string VisualBasicDefaultExt = "vb"; + internal static string TestProjectName = "TestProject"; + + #region Get Diagnostics + + /// <summary> + /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. + /// </summary> + /// <param name="sources">Classes in the form of strings</param> + /// <param name="language">The language the source classes are in</param> + /// <param name="analyzer">The analyzer to be run on the sources</param> + /// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns> + private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) + { + return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); + } + + /// <summary> + /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. + /// The returned diagnostics are then ordered by location in the source document. + /// </summary> + /// <param name="analyzer">The analyzer to run on the documents</param> + /// <param name="documents">The Documents that the analyzer will be run on</param> + /// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns> + protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) + { + var projects = new HashSet<Project>(); + foreach (var document in documents) + { + projects.Add(document.Project); + } + + var diagnostics = new List<Diagnostic>(); + foreach (var project in projects) + { + var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); + var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; + foreach (var diag in diags) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + for (int i = 0; i < documents.Length; i++) + { + var document = documents[i]; + var tree = document.GetSyntaxTreeAsync().Result; + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + + var results = SortDiagnostics(diagnostics); + diagnostics.Clear(); + return results; + } + + /// <summary> + /// Sort diagnostics by location in source document + /// </summary> + /// <param name="diagnostics">The list of Diagnostics to be sorted</param> + /// <returns>An IEnumerable containing the Diagnostics in order of Location</returns> + private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics) + { + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + } + + #endregion + + #region Set up compilation and documents + /// <summary> + /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. + /// </summary> + /// <param name="sources">Classes in the form of strings</param> + /// <param name="language">The language the source code is in</param> + /// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns> + private static Document[] GetDocuments(string[] sources, string language) + { + if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) + { + throw new ArgumentException("Unsupported Language"); + } + + var project = CreateProject(sources, language); + var documents = project.Documents.ToArray(); + + if (sources.Length != documents.Length) + { + throw new InvalidOperationException("Amount of sources did not match amount of Documents created"); + } + + return documents; + } + + /// <summary> + /// Create a Document from a string through creating a project that contains it. + /// </summary> + /// <param name="source">Classes in the form of a string</param> + /// <param name="language">The language the source code is in</param> + /// <returns>A Document created from the source string</returns> + protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) + { + return CreateProject(new[] { source }, language).Documents.First(); + } + + /// <summary> + /// Create a project using the inputted strings as sources. + /// </summary> + /// <param name="sources">Classes in the form of strings</param> + /// <param name="language">The language the source code is in</param> + /// <returns>A Project created out of the Documents created from the source strings</returns> + private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) + { + string fileNamePrefix = DefaultFilePathPrefix; + string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; + + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + + var solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, TestProjectName, TestProjectName, language) + .AddMetadataReference(projectId, CorlibReference) + .AddMetadataReference(projectId, SystemCoreReference) + .AddMetadataReference(projectId, CSharpSymbolsReference) + .AddMetadataReference(projectId, CodeAnalysisReference) + .AddMetadataReference(projectId, LuceneNetReference); + + int count = 0; + foreach (var source in sources) + { + var newFileName = fileNamePrefix + count + "." + fileExt; + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); + count++; + } + return solution.GetProject(projectId); + } + #endregion + } +} + diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Lucene.Net.Tests.CodeAnalysis.csproj b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Lucene.Net.Tests.CodeAnalysis.csproj new file mode 100644 index 0000000..b94c9c6 --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Lucene.Net.Tests.CodeAnalysis.csproj @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +--> +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp2.2</TargetFramework> + <RootNamespace>Lucene.Net.CodeAnalysis</RootNamespace> + </PropertyGroup> + + <Import Project="$(SolutionDir)build/TestReferences.Common.targets" /> + + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="$(MicrosoftCodeAnalysisAnalyzersPackageVersion)" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)" /> + <PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion)" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Lucene.Net\Lucene.Net.csproj" /> + <ProjectReference Include="..\Lucene.Net.CodeAnalysis\Lucene.Net.CodeAnalysis.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealIncrementTokenMethodCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealIncrementTokenMethodCSCodeFixProvider.cs new file mode 100644 index 0000000..cace5b3 --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealIncrementTokenMethodCSCodeFixProvider.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; +using System; +using TestHelper; + +namespace Lucene.Net.CodeAnalysis +{ + public class TestLucene1000_SealIncrementTokenMethodCSCodeFixProvider : CodeFixVerifier + { + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new Lucene1000_SealIncrementTokenMethodCSCodeFixProvider(); + } + + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + { + return new Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer(); + } + + + //No diagnostics expected to show up + [Test] + public void TestEmptyFile() + { + var test = @""; + + VerifyCSharpDiagnostic(test); + } + + + //Diagnostic and CodeFix both triggered and checked for + [Test] + public void TestDiagnosticAndCodeFix() + { + var test = @" +using Lucene.Net.Analysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace MyNamespace +{ + class TypeName : TokenStream + { + public override bool IncrementToken() + { + throw new NotImplementedException(); + } + } +}"; + var expected = new DiagnosticResult + { + Id = Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.DiagnosticId, + Message = String.Format("Type name '{0}' or its IncrementToken() method must be marked sealed.", "TypeName"), + Severity = DiagnosticSeverity.Error, + Locations = + new[] { + new DiagnosticResultLocation("Test0.cs", 12, 5) + } + }; + + VerifyCSharpDiagnostic(test, expected); + + var fixtest = @" +using Lucene.Net.Analysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace MyNamespace +{ + class TypeName : TokenStream + { + public sealed override bool IncrementToken() + { + throw new NotImplementedException(); + } + } +}"; + VerifyCSharpFix(test, fixtest); + } + } +} diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealIncrementTokenMethodVBCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealIncrementTokenMethodVBCodeFixProvider.cs new file mode 100644 index 0000000..e1ab51d --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealIncrementTokenMethodVBCodeFixProvider.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; +using System; +using TestHelper; + +namespace Lucene.Net.CodeAnalysis +{ + public class TestLucene1000_SealIncrementTokenMethodVBCodeFixProvider : CodeFixVerifier + { + protected override CodeFixProvider GetBasicCodeFixProvider() + { + return new Lucene1000_SealIncrementTokenMethodVBCodeFixProvider(); + } + + protected override DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() + { + return new Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer(); + } + + + //No diagnostics expected to show up + [Test] + public void TestEmptyFile() + { + var test = @""; + + VerifyBasicDiagnostic(test); + } + + + //Diagnostic and CodeFix both triggered and checked for + [Test] + public void TestDiagnosticAndCodeFix() + { + var test = @" +Imports Lucene.Net.Analysis +Imports System +Imports System.Collections.Generic +Imports System.Linq +Imports System.Text +Imports System.Threading.Tasks +Imports System.Diagnostics + +Namespace MyNamespace + Class TypeName + Inherits TokenStream + + Public Overrides Function IncrementToken() As Boolean + Throw New NotImplementedException() + End Function + + End Class +End Namespace"; + var expected = new DiagnosticResult + { + Id = Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.DiagnosticId, + Message = String.Format("Type name '{0}' must be marked NotInheritable or its IncrementToken() method must be marked NotOverridable.", "TypeName"), + Severity = DiagnosticSeverity.Error, + Locations = + new[] { + new DiagnosticResultLocation("Test0.vb", 11, 5) + } + }; + + VerifyBasicDiagnostic(test, expected); + + var fixtest = @" +Imports Lucene.Net.Analysis +Imports System +Imports System.Collections.Generic +Imports System.Linq +Imports System.Text +Imports System.Threading.Tasks +Imports System.Diagnostics + +Namespace MyNamespace + Class TypeName + Inherits TokenStream + + Public NotOverridable Overrides Function IncrementToken() As Boolean + Throw New NotImplementedException() + End Function + + End Class +End Namespace"; + VerifyBasicFix(test, fixtest); + } + } +} diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealTokenStreamClassCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealTokenStreamClassCSCodeFixProvider.cs new file mode 100644 index 0000000..82f6f2d --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1000_SealTokenStreamClassCSCodeFixProvider.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; +using System; +using TestHelper; + +namespace Lucene.Net.CodeAnalysis +{ + public class TestLucene1000_SealTokenStreamClassCSCodeFixProvider : CodeFixVerifier + { + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new Lucene1000_SealTokenStreamClassCSCodeFixProvider(); + } + + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + { + return new Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer(); + } + + + //No diagnostics expected to show up + [Test] + public void TestEmptyFile() + { + var test = @""; + + VerifyCSharpDiagnostic(test); + } + + + //Diagnostic and CodeFix both triggered and checked for + [Test] + public void TestDiagnosticAndCodeFix() + { + var test = @" +using Lucene.Net.Analysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace MyNamespace +{ + class TypeName : TokenStream + { + public override bool IncrementToken() + { + throw new NotImplementedException(); + } + } +}"; + var expected = new DiagnosticResult + { + Id = Lucene1000_TokenStreamOrItsIncrementTokenMethodMustBeSealedAnalyzer.DiagnosticId, + Message = String.Format("Type name '{0}' or its IncrementToken() method must be marked sealed.", "TypeName"), + Severity = DiagnosticSeverity.Error, + Locations = + new[] { + new DiagnosticResultLocation("Test0.cs", 12, 5) + } + }; + + VerifyCSharpDiagnostic(test, expected); + + var fixtest = @" +using Lucene.Net.Analysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace MyNamespace +{ + sealed class TypeName : TokenStream + { + public override bool IncrementToken() + { + throw new NotImplementedException(); + } + } +}"; + VerifyCSharpFix(test, fixtest); + } + } +} diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Verifiers/CodeFixVerifier.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Verifiers/CodeFixVerifier.cs new file mode 100644 index 0000000..b40bddb --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Verifiers/CodeFixVerifier.cs @@ -0,0 +1,128 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Formatting; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace TestHelper +{ + /// <summary> + /// Superclass of all Unit tests made for diagnostics with codefixes. + /// Contains methods used to verify correctness of codefixes + /// </summary> + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// <summary> + /// Returns the codefix being tested (C#) - to be implemented in non-abstract class + /// </summary> + /// <returns>The CodeFixProvider to be used for CSharp code</returns> + protected virtual CodeFixProvider GetCSharpCodeFixProvider() + { + return null; + } + + /// <summary> + /// Returns the codefix being tested (VB) - to be implemented in non-abstract class + /// </summary> + /// <returns>The CodeFixProvider to be used for VisualBasic code</returns> + protected virtual CodeFixProvider GetBasicCodeFixProvider() + { + return null; + } + + /// <summary> + /// Called to test a C# codefix when applied on the inputted string as a source + /// </summary> + /// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param> + /// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param> + /// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param> + /// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param> + protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) + { + VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); + } + + /// <summary> + /// Called to test a VB codefix when applied on the inputted string as a source + /// </summary> + /// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param> + /// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param> + /// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param> + /// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param> + protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) + { + VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); + } + + /// <summary> + /// General verifier for codefixes. + /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. + /// Then gets the string after the codefix is applied and compares it with the expected result. + /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. + /// </summary> + /// <param name="language">The language the source code is in</param> + /// <param name="analyzer">The analyzer to be applied to the source code</param> + /// <param name="codeFixProvider">The codefix to be applied to the code wherever the relevant Diagnostic is found</param> + /// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param> + /// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param> + /// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param> + /// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param> + private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) + { + var document = CreateDocument(oldSource, language); + var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); + var compilerDiagnostics = GetCompilerDiagnostics(document); + var attempts = analyzerDiagnostics.Length; + + for (int i = 0; i < attempts; ++i) + { + var actions = new List<CodeAction>(); + var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); + codeFixProvider.RegisterCodeFixesAsync(context).Wait(); + + if (!actions.Any()) + { + break; + } + + if (codeFixIndex != null) + { + document = ApplyFix(document, actions.ElementAt((int)codeFixIndex)); + break; + } + + document = ApplyFix(document, actions.ElementAt(0)); + analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); + + var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); + + //check if applying the code fix introduced any new compiler diagnostics + if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) + { + // Format and get the compiler diagnostics again so that the locations make sense in the output + document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); + newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); + + Assert.IsTrue(false, + string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", + string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), + document.GetSyntaxRootAsync().Result.ToFullString())); + } + + //check if there are analyzer diagnostics left after the code fix + if (!analyzerDiagnostics.Any()) + { + break; + } + } + + //after applying all of the code fixes, compare the resulting string to the inputted one + var actual = GetStringFromDocument(document); + Assert.AreEqual(newSource, actual); + } + } +} diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Verifiers/DiagnosticVerifier.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Verifiers/DiagnosticVerifier.cs new file mode 100644 index 0000000..d41f753 --- /dev/null +++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Verifiers/DiagnosticVerifier.cs @@ -0,0 +1,271 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TestHelper +{ + /// <summary> + /// Superclass of all Unit Tests for DiagnosticAnalyzers + /// </summary> + [TestFixture] + public abstract partial class DiagnosticVerifier + { + #region To be implemented by Test classes + /// <summary> + /// Get the CSharp analyzer being tested - to be implemented in non-abstract class + /// </summary> + protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + { + return null; + } + + /// <summary> + /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class + /// </summary> + protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() + { + return null; + } + #endregion + + #region Verifier wrappers + + /// <summary> + /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// </summary> + /// <param name="source">A class in the form of a string to run the analyzer on</param> + /// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param> + protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// <summary> + /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// </summary> + /// <param name="source">A class in the form of a string to run the analyzer on</param> + /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the source</param> + protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + } + + /// <summary> + /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// </summary> + /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param> + /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param> + protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// <summary> + /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// </summary> + /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param> + /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param> + protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + } + + /// <summary> + /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, + /// then verifies each of them. + /// </summary> + /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param> + /// <param name="language">The language of the classes represented by the source strings</param> + /// <param name="analyzer">The analyzer to be run on the source code</param> + /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param> + private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) + { + var diagnostics = GetSortedDiagnostics(sources, language, analyzer); + VerifyDiagnosticResults(diagnostics, analyzer, expected); + } + + #endregion + + #region Actual comparisons and verifications + /// <summary> + /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. + /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. + /// </summary> + /// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param> + /// <param name="analyzer">The analyzer that was being run on the sources</param> + /// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param> + private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) + { + int expectedCount = expectedResults.Count(); + int actualCount = actualResults.Count(); + + if (expectedCount != actualCount) + { + string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; + + Assert.IsTrue(false, + string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); + } + + for (int i = 0; i < expectedResults.Length; i++) + { + var actual = actualResults.ElementAt(i); + var expected = expectedResults[i]; + + if (expected.Line == -1 && expected.Column == -1) + { + if (actual.Location != Location.None) + { + Assert.IsTrue(false, + string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", + FormatDiagnostics(analyzer, actual))); + } + } + else + { + VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); + var additionalLocations = actual.AdditionalLocations.ToArray(); + + if (additionalLocations.Length != expected.Locations.Length - 1) + { + Assert.IsTrue(false, + string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", + expected.Locations.Length - 1, additionalLocations.Length, + FormatDiagnostics(analyzer, actual))); + } + + for (int j = 0; j < additionalLocations.Length; ++j) + { + VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); + } + } + + if (actual.Id != expected.Id) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); + } + + if (actual.Severity != expected.Severity) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); + } + + if (actual.GetMessage() != expected.Message) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); + } + } + } + + /// <summary> + /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. + /// </summary> + /// <param name="analyzer">The analyzer that was being run on the sources</param> + /// <param name="diagnostic">The diagnostic that was found in the code</param> + /// <param name="actual">The Location of the Diagnostic found in the code</param> + /// <param name="expected">The DiagnosticResultLocation that should have been found</param> + private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) + { + var actualSpan = actual.GetLineSpan(); + + Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), + string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); + + var actualLinePosition = actualSpan.StartLinePosition; + + // Only check line position if there is an actual line in the real diagnostic + if (actualLinePosition.Line > 0) + { + if (actualLinePosition.Line + 1 != expected.Line) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + + // Only check column position if there is an actual column position in the real diagnostic + if (actualLinePosition.Character > 0) + { + if (actualLinePosition.Character + 1 != expected.Column) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + } + #endregion + + #region Formatting Diagnostics + /// <summary> + /// Helper method to format a Diagnostic into an easily readable string + /// </summary> + /// <param name="analyzer">The analyzer that this verifier tests</param> + /// <param name="diagnostics">The Diagnostics to be formatted</param> + /// <returns>The Diagnostics formatted as a string</returns> + private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) + { + var builder = new StringBuilder(); + for (int i = 0; i < diagnostics.Length; ++i) + { + builder.AppendLine("// " + diagnostics[i].ToString()); + + var analyzerType = analyzer.GetType(); + var rules = analyzer.SupportedDiagnostics; + + foreach (var rule in rules) + { + if (rule != null && rule.Id == diagnostics[i].Id) + { + var location = diagnostics[i].Location; + if (location == Location.None) + { + builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); + } + else + { + Assert.IsTrue(location.IsInSource, + $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); + + string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; + var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; + + builder.AppendFormat("{0}({1}, {2}, {3}.{4})", + resultMethodName, + linePosition.Line + 1, + linePosition.Character + 1, + analyzerType.Name, + rule.Id); + } + + if (i != diagnostics.Length - 1) + { + builder.Append(','); + } + + builder.AppendLine(); + break; + } + } + } + return builder.ToString(); + } + #endregion + } +}
