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
+    }
+}

Reply via email to