You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by pa...@apache.org on 2026/05/28 03:04:47 UTC

(lucenenet) branch master updated: Add Lucene1001 / Lucene1002 analyzers for TokenStream contract, #1146 (#1288)

This is an automated email from the ASF dual-hosted git repository.

paulirwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git


The following commit(s) were added to refs/heads/master by this push:
     new ffbfb36f9 Add Lucene1001 / Lucene1002 analyzers for TokenStream contract, #1146 (#1288)
ffbfb36f9 is described below

commit ffbfb36f9227e3acd34b05ec9aa6d8a4854c6c6b
Author: Paul Irwin <pa...@gmail.com>
AuthorDate: Wed May 27 21:04:41 2026 -0600

    Add Lucene1001 / Lucene1002 analyzers for TokenStream contract, #1146 (#1288)
    
    * Add Lucene1001 / Lucene1002 analyzers for TokenStream contract (#1146)
    
    Adds two new public Roslyn analyzers (C# + VB) that surface the
    TokenStream subclassing rules at compile time as warnings instead of
    runtime exceptions:
    
    - Lucene1001: an override of End()/Reset()/Close() on a TokenStream
      subclass must call the corresponding base method.
    - Lucene1002: a non-abstract Tokenizer subclass must override End()
      (passes if any class in the hierarchy between the subclass and
      Tokenizer already provides the override).
    
    Both analyzers ship with code fix providers. The primary audience is
    consumers of the library who write their own TokenStream-derived
    types — Lucene.NET's in-tree TokenStream subclasses intentionally
    match the upstream Java behavior, which itself does not always call
    super.X() in End/Reset/Close. Each in-tree violation is therefore
    suppressed with [SuppressMessage] and a "LUCENENET specific" comment
    that explains why it matches the Java parent. Two End() overrides
    (PrefixAwareTokenFilter, PrefixAndSuffixAwareTokenFilter) carry an
    additional "LUCENENET TODO" because the upstream Java omission of
    super.end() may be a latent bug worth revisiting.
    
    The test verifier helper now references System.Runtime so analyzer
    fixes that introduce base calls returning void don't trigger a
    spurious CS0012 in the post-fix compilation check.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
    
    * Address Copilot review feedback on Lucene1001/Lucene1002 analyzers
    
    - Lucene1001 CS code fix: emit ThrowStatement (not ExpressionStatement) when
      converting an expression-bodied `=> throw ...` member, so the produced code
      is valid C#.
    - Lucene1001 CS + VB analyzers: exclude lambdas, anonymous methods, and (CS)
      local functions from the base-call scan. A base call inside an uninvoked
      delegate body should not satisfy the contract.
    - Lucene1002 CS + VB code fixes: use ElasticCarriageReturnLineFeed instead of
      hardcoded line endings so the formatter normalizes to the document's
      convention.
    - DiagnosticVerifier: harden System.Runtime.dll lookup with a
      TRUSTED_PLATFORM_ASSEMBLIES fallback and a clear error if neither resolves.
    - Tests: add VerifyCSharpFix/VerifyBasicFix coverage for Lucene1002, a
      VerifyCSharpFix for the Lucene1001 expression-bodied case plus a new
      throw-expression regression, lambda/local-function exclusion tests for
      Lucene1001 (CS + VB), and the missing VB indirect-Tokenizer-inheritance
      test for Lucene1002.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <no...@anthropic.com>
---
 .../PrefixAndSuffixAwareTokenFilter.cs             |  15 +
 .../Miscellaneous/PrefixAwareTokenFilter.cs        |  11 +
 .../Miscellaneous/SingleTokenTokenStream.cs        |   5 +
 .../Analysis/Sinks/TeeSinkTokenFilter.cs           |   5 +
 .../Analysis/Synonym/SlowSynonymFilter.cs          |   6 +
 .../DoubleMetaphoneFilter.cs                       |   5 +
 src/Lucene.Net.Analysis.Phonetic/PhoneticFilter.cs |   5 +
 .../Highlight/TokenStreamFromTermPositionVector.cs |   4 +
 src/Lucene.Net/Analysis/CachingTokenFilter.cs      |   5 +
 src/Lucene.Net/Analysis/NumericTokenStream.cs      |   4 +
 src/Lucene.Net/Analysis/TokenFilter.cs             |   5 +
 src/Lucene.Net/Document/Field.cs                   |   7 +
 ...ucene1001_AddBaseMethodCallCSCodeFixProvider.cs | 112 ++++++
 ...enStreamOverrideMustCallBaseMethodCSAnalyzer.cs | 168 +++++++++
 .../Lucene1002_AddEndOverrideCSCodeFixProvider.cs  | 111 ++++++
 ...ucene1002_TokenizerMustOverrideEndCSAnalyzer.cs | 119 ++++++
 ...ucene1001_AddBaseMethodCallVBCodeFixProvider.cs |  88 +++++
 ...enStreamOverrideMustCallBaseMethodVBAnalyzer.cs | 141 +++++++
 .../Lucene1002_AddEndOverrideVBCodeFixProvider.cs  | 108 ++++++
 ...ucene1002_TokenizerMustOverrideEndVBAnalyzer.cs | 110 ++++++
 .../Helpers/DiagnosticVerifier.Helper.cs           |  36 ++
 ...ucene1001_AddBaseMethodCallCSCodeFixProvider.cs | 418 +++++++++++++++++++++
 ...ucene1001_AddBaseMethodCallVBCodeFixProvider.cs | 177 +++++++++
 ...stLucene1002_AddEndOverrideCSCodeFixProvider.cs | 218 +++++++++++
 ...stLucene1002_AddEndOverrideVBCodeFixProvider.cs | 227 +++++++++++
 25 files changed, 2110 insertions(+)

diff --git a/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAndSuffixAwareTokenFilter.cs b/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAndSuffixAwareTokenFilter.cs
index ba37dc1c8..e6d0fb524 100644
--- a/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAndSuffixAwareTokenFilter.cs
+++ b/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAndSuffixAwareTokenFilter.cs
@@ -1,4 +1,6 @@
 // Lucene version compatibility level 4.8.1
+using System.Diagnostics.CodeAnalysis;
+
 namespace Lucene.Net.Analysis.Miscellaneous
 {
     /*
@@ -85,16 +87,29 @@ public sealed override bool IncrementToken()
             return suffix.IncrementToken();
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which does not call super.reset().
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenStream.Reset() is a no-op so the omission is equivalent.")]
         public override void Reset()
         {
             suffix.Reset();
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which does not call super.close().
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenStream.Close() is a no-op so the omission is equivalent.")]
         public override void Close()
         {
             suffix.Close();
         }
 
+        // LUCENENET TODO: the upstream Java does not call super.end() here, so neither do we — but
+        // TokenStream.End() does ClearAttributes() and resets PositionIncrementAttribute, which the
+        // documented contract says End() overrides should do. This may be a latent bug in upstream
+        // Lucene that the .NET port should investigate; see Lucene.NET issue tracker if reproducing
+        // a problem with end-of-stream attribute state on this filter.
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; see LUCENENET TODO above for why this may need to be revisited.")]
         public override void End()
         {
             suffix.End();
diff --git a/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAwareTokenFilter.cs b/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAwareTokenFilter.cs
index e72700d0b..040450c20 100644
--- a/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAwareTokenFilter.cs
+++ b/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/PrefixAwareTokenFilter.cs
@@ -1,6 +1,7 @@
 // Lucene version compatibility level 4.8.1
 using Lucene.Net.Analysis.TokenAttributes;
 using Lucene.Net.Util;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Lucene.Net.Analysis.Miscellaneous
 {
@@ -169,12 +170,22 @@ public virtual Token UpdateSuffixToken(Token suffixToken, Token lastPrefixToken)
             return suffixToken;
         }
 
+        // LUCENENET TODO: the upstream Java does not call super.end() here, so neither do we — but
+        // TokenStream.End() does ClearAttributes() and resets PositionIncrementAttribute, which the
+        // documented contract says End() overrides should do. This may be a latent bug in upstream
+        // Lucene that the .NET port should investigate; see Lucene.NET issue tracker if reproducing
+        // a problem with end-of-stream attribute state on this filter.
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; see LUCENENET TODO above for why this may need to be revisited.")]
         public override void End()
         {
             prefix.End();
             suffix.End();
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which does not call super.close().
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenStream.Close() is a no-op so the omission is equivalent.")]
         public override void Close()
         {
             prefix.Close();
diff --git a/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/SingleTokenTokenStream.cs b/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/SingleTokenTokenStream.cs
index 5549a688d..2cc9cd599 100644
--- a/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/SingleTokenTokenStream.cs
+++ b/src/Lucene.Net.Analysis.Common/Analysis/Miscellaneous/SingleTokenTokenStream.cs
@@ -2,6 +2,7 @@
 using Lucene.Net.Analysis.TokenAttributes;
 using Lucene.Net.Diagnostics;
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Lucene.Net.Analysis.Miscellaneous
 {
@@ -58,6 +59,10 @@ public override sealed bool IncrementToken()
             }
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which does not call super.reset().
+        // TokenStream.Reset() has an empty body so this is equivalent.
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenStream.Reset() is a no-op so the omission is equivalent.")]
         public override void Reset()
         {
             exhausted = false;
diff --git a/src/Lucene.Net.Analysis.Common/Analysis/Sinks/TeeSinkTokenFilter.cs b/src/Lucene.Net.Analysis.Common/Analysis/Sinks/TeeSinkTokenFilter.cs
index a78061906..958f59167 100644
--- a/src/Lucene.Net.Analysis.Common/Analysis/Sinks/TeeSinkTokenFilter.cs
+++ b/src/Lucene.Net.Analysis.Common/Analysis/Sinks/TeeSinkTokenFilter.cs
@@ -2,6 +2,7 @@
 using Lucene.Net.Util;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using JCG = J2N.Collections.Generic;
 
 namespace Lucene.Net.Analysis.Sinks
@@ -248,6 +249,8 @@ public override sealed bool IncrementToken()
                 return true;
             }
 
+            [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+                Justification = "Sink streams replay cached state; calling base.End() would clear the attributes that RestoreState then sets. Matches Lucene Java behavior.")]
             public override sealed void End()
             {
                 if (finalState != null)
@@ -256,6 +259,8 @@ public override sealed void End()
                 }
             }
 
+            [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+                Justification = "Sink streams replay cached state; the iterator reset is sufficient and base.Reset() (no-op on TokenStream) provides nothing further. Matches Lucene Java behavior.")]
             public override sealed void Reset()
             {
                 it = cachedStates.GetEnumerator();
diff --git a/src/Lucene.Net.Analysis.Common/Analysis/Synonym/SlowSynonymFilter.cs b/src/Lucene.Net.Analysis.Common/Analysis/Synonym/SlowSynonymFilter.cs
index 97ddea64e..eb379eadd 100644
--- a/src/Lucene.Net.Analysis.Common/Analysis/Synonym/SlowSynonymFilter.cs
+++ b/src/Lucene.Net.Analysis.Common/Analysis/Synonym/SlowSynonymFilter.cs
@@ -3,6 +3,7 @@
 using Lucene.Net.Util;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using JCG = J2N.Collections.Generic;
 
 namespace Lucene.Net.Analysis.Synonym
@@ -309,6 +310,11 @@ private void Copy(AttributeSource target, AttributeSource source)
             }
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which calls input.reset()
+        // directly rather than super.reset(). Behavior is identical (TokenFilter.Reset() just chains
+        // to m_input.Reset()), but we keep the call shape to preserve Java parity.
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenFilter.Reset() chains to m_input.Reset() so this is equivalent.")]
         public override void Reset()
         {
             m_input.Reset();
diff --git a/src/Lucene.Net.Analysis.Phonetic/DoubleMetaphoneFilter.cs b/src/Lucene.Net.Analysis.Phonetic/DoubleMetaphoneFilter.cs
index a8f41ede9..06585f003 100644
--- a/src/Lucene.Net.Analysis.Phonetic/DoubleMetaphoneFilter.cs
+++ b/src/Lucene.Net.Analysis.Phonetic/DoubleMetaphoneFilter.cs
@@ -5,6 +5,7 @@
 using Lucene.Net.Support;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Lucene.Net.Analysis.Phonetic
 {
@@ -118,6 +119,10 @@ public override bool IncrementToken()
             }
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which calls input.reset()
+        // directly rather than super.reset(). Behavior is identical.
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenFilter.Reset() chains to m_input.Reset() so this is equivalent.")]
         public override void Reset()
         {
             m_input.Reset();
diff --git a/src/Lucene.Net.Analysis.Phonetic/PhoneticFilter.cs b/src/Lucene.Net.Analysis.Phonetic/PhoneticFilter.cs
index c4ed239a7..e5318bc68 100644
--- a/src/Lucene.Net.Analysis.Phonetic/PhoneticFilter.cs
+++ b/src/Lucene.Net.Analysis.Phonetic/PhoneticFilter.cs
@@ -3,6 +3,7 @@
 using Lucene.Net.Analysis.TokenAttributes;
 using Lucene.Net.Analysis.TokenAttributes.Extensions;
 using System;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Lucene.Net.Analysis.Phonetic
 {
@@ -104,6 +105,10 @@ public override bool IncrementToken()
             return true;
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which calls input.reset()
+        // directly rather than super.reset(). Behavior is identical.
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenFilter.Reset() chains to m_input.Reset() so this is equivalent.")]
         public override void Reset()
         {
             m_input.Reset();
diff --git a/src/Lucene.Net.Highlighter/Highlight/TokenStreamFromTermPositionVector.cs b/src/Lucene.Net.Highlighter/Highlight/TokenStreamFromTermPositionVector.cs
index 7ce03995a..cdb1e06ef 100644
--- a/src/Lucene.Net.Highlighter/Highlight/TokenStreamFromTermPositionVector.cs
+++ b/src/Lucene.Net.Highlighter/Highlight/TokenStreamFromTermPositionVector.cs
@@ -4,6 +4,7 @@
 using Lucene.Net.Index;
 using Lucene.Net.Util;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using JCG = J2N.Collections.Generic;
 
 namespace Lucene.Net.Search.Highlight
@@ -126,6 +127,9 @@ public override bool IncrementToken()
             return false;
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which does not call super.reset().
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenStream.Reset() is a no-op so the omission is equivalent.")]
         public override void Reset()
         {
             this.tokensAtCurrentPosition = this.positionedTokens.GetEnumerator();
diff --git a/src/Lucene.Net/Analysis/CachingTokenFilter.cs b/src/Lucene.Net/Analysis/CachingTokenFilter.cs
index 613968fc7..5096508d0 100644
--- a/src/Lucene.Net/Analysis/CachingTokenFilter.cs
+++ b/src/Lucene.Net/Analysis/CachingTokenFilter.cs
@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using JCG = J2N.Collections.Generic;
 
 namespace Lucene.Net.Analysis
@@ -67,6 +68,8 @@ public override bool IncrementToken()
             return true;
         }
 
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Caching filter intentionally restores the previously captured final state instead of chaining to base.End(); matches Lucene Java behavior.")]
         public override void End()
         {
             if (finalState != null)
@@ -82,6 +85,8 @@ public override void End()
         /// the first time. You should <see cref="Reset()"/> the inner tokenstream before wrapping
         /// it with <see cref="CachingTokenFilter"/>.
         /// </summary>
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Caching filter intentionally does not call base.Reset() (which would propagate to the wrapped input); matches Lucene Java behavior. See the doc comment.")]
         public override void Reset()
         {
             if (cache != null)
diff --git a/src/Lucene.Net/Analysis/NumericTokenStream.cs b/src/Lucene.Net/Analysis/NumericTokenStream.cs
index 79f3f50ae..d4e29306c 100644
--- a/src/Lucene.Net/Analysis/NumericTokenStream.cs
+++ b/src/Lucene.Net/Analysis/NumericTokenStream.cs
@@ -2,6 +2,7 @@
 using Lucene.Net.Diagnostics;
 using Lucene.Net.Util;
 using System;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Lucene.Net.Analysis
 {
@@ -330,6 +331,9 @@ public NumericTokenStream SetSingleValue(float value)
             return this;
         }
 
+        // LUCENENET specific: matches the upstream Java implementation, which does not call super.reset().
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "Matches upstream Lucene Java behavior; TokenStream.Reset() is a no-op so the omission is equivalent.")]
         public override void Reset()
         {
             if (valSize == 0)
diff --git a/src/Lucene.Net/Analysis/TokenFilter.cs b/src/Lucene.Net/Analysis/TokenFilter.cs
index 83313ecdf..583e656b3 100644
--- a/src/Lucene.Net/Analysis/TokenFilter.cs
+++ b/src/Lucene.Net/Analysis/TokenFilter.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 
 namespace Lucene.Net.Analysis
@@ -58,6 +59,8 @@ protected TokenFilter(TokenStream input)
         /// be sure to call <c>base.End()</c> first when overriding this method.
         /// </remarks>
         /// <exception cref="IOException"> If an I/O error occurs </exception>
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "TokenFilter intentionally chains the call to the wrapped input rather than calling base.End(); this matches the Lucene Java implementation. Subclasses that override TokenFilter.End() are still expected to call base.End() (i.e. this method).")]
         public override void End()
         {
             m_input.End();
@@ -85,6 +88,8 @@ public override void Close()
         /// The default implementation chains the call to the input <see cref="TokenStream"/>, so
         /// be sure to call <c>base.Reset()</c> when overriding this method.
         /// </remarks>
+        [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+            Justification = "TokenFilter intentionally chains the call to the wrapped input rather than calling base.Reset(); this matches the Lucene Java implementation. Subclasses that override TokenFilter.Reset() are still expected to call base.Reset() (i.e. this method).")]
         public override void Reset()
         {
             m_input.Reset();
diff --git a/src/Lucene.Net/Document/Field.cs b/src/Lucene.Net/Document/Field.cs
index 212365a81..4845ea76c 100644
--- a/src/Lucene.Net/Document/Field.cs
+++ b/src/Lucene.Net/Document/Field.cs
@@ -3,6 +3,7 @@
 using Lucene.Net.Index;
 using Lucene.Net.Util;
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
 using System.Text;
@@ -949,11 +950,17 @@ public override void End()
                 offsetAttribute.SetOffset(finalOffset, finalOffset);
             }
 
+            // LUCENENET specific: matches the upstream Java implementation, which does not call super.reset().
+            [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+                Justification = "Matches upstream Lucene Java behavior; TokenStream.Reset() is a no-op so the omission is equivalent.")]
             public override void Reset()
             {
                 used = false;
             }
 
+            // LUCENENET specific: matches the upstream Java implementation, which does not call super.close().
+            [SuppressMessage("Design", "Lucene1001:TokenStream override of End()/Reset()/Close() must call the corresponding base method.",
+                Justification = "Matches upstream Lucene Java behavior; TokenStream.Close() is a no-op so the omission is equivalent.")]
             public override void Close()
             {
                 value = null;
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1001_AddBaseMethodCallCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1001_AddBaseMethodCallCSCodeFixProvider.cs
new file mode 100644
index 000000000..ab62668d8
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1001_AddBaseMethodCallCSCodeFixProvider.cs
@@ -0,0 +1,112 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Lucene1001_AddBaseMethodCallCSCodeFixProvider)), Shared]
+    public class Lucene1001_AddBaseMethodCallCSCodeFixProvider : CodeFixProvider
+    {
+        public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId);
+
+        public sealed override FixAllProvider GetFixAllProvider()
+        {
+            return WellKnownFixAllProviders.BatchFixer;
+        }
+
+        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+        {
+            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+            var diagnostic = context.Diagnostics.First();
+            var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+            var methodDeclaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().FirstOrDefault();
+            if (methodDeclaration is null)
+            {
+                return;
+            }
+
+            var methodName = methodDeclaration.Identifier.ValueText;
+            var title = $"Add base.{methodName}() call";
+
+            context.RegisterCodeFix(
+                CodeAction.Create(
+                    title: title,
+                    createChangedDocument: c => AddBaseCallAsync(context.Document, methodDeclaration, c),
+                    equivalenceKey: title),
+                diagnostic);
+        }
+
+        private static async Task<Document> AddBaseCallAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
+        {
+            var methodName = methodDeclaration.Identifier.ValueText;
+
+            var baseInvocation = SyntaxFactory.InvocationExpression(
+                SyntaxFactory.MemberAccessExpression(
+                    SyntaxKind.SimpleMemberAccessExpression,
+                    SyntaxFactory.BaseExpression(),
+                    SyntaxFactory.IdentifierName(methodName)));
+
+            var baseStatement = SyntaxFactory.ExpressionStatement(baseInvocation)
+                .WithAdditionalAnnotations(Formatter.Annotation);
+
+            MethodDeclarationSyntax newMethodDeclaration;
+
+            if (methodDeclaration.Body is not null)
+            {
+                var newBody = methodDeclaration.Body.WithStatements(
+                    methodDeclaration.Body.Statements.Insert(0, baseStatement));
+                newMethodDeclaration = methodDeclaration.WithBody(newBody);
+            }
+            else if (methodDeclaration.ExpressionBody is not null)
+            {
+                // Convert expression body => to a block containing base.X(); followed by the original expression as a statement.
+                var existingExpression = methodDeclaration.ExpressionBody.Expression;
+                StatementSyntax existingStatement = existingExpression is ThrowExpressionSyntax throwExpression
+                    ? SyntaxFactory.ThrowStatement(throwExpression.Expression)
+                    : SyntaxFactory.ExpressionStatement(existingExpression);
+                existingStatement = existingStatement.WithAdditionalAnnotations(Formatter.Annotation);
+                var newBody = SyntaxFactory.Block(baseStatement, existingStatement)
+                    .WithAdditionalAnnotations(Formatter.Annotation);
+                newMethodDeclaration = methodDeclaration
+                    .WithExpressionBody(null)
+                    .WithSemicolonToken(default)
+                    .WithBody(newBody);
+            }
+            else
+            {
+                return document;
+            }
+
+            var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+            var newRoot = oldRoot.ReplaceNode(methodDeclaration, newMethodDeclaration);
+            return document.WithSyntaxRoot(newRoot);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.cs b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.cs
new file mode 100644
index 000000000..9ebd0c137
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.cs
@@ -0,0 +1,168 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    // LUCENENET: TokenStream documents that subclasses overriding End(), Reset(), or Close()
+    // must call the corresponding base method, otherwise some internal state will not be
+    // correctly reset and Tokenizer will throw at runtime. This analyzer surfaces the same
+    // contract at compile time. See: src/Lucene.Net/Analysis/TokenStream.cs and the
+    // "More Requirements for Analysis Component Classes" section of Analysis package.md.
+    [DiagnosticAnalyzer(LanguageNames.CSharp)]
+    public class Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer : DiagnosticAnalyzer
+    {
+        public const string DiagnosticId = "Lucene1001";
+        private const string Category = "Design";
+
+        private const string Title = "TokenStream override of End()/Reset()/Close() must call the corresponding base method.";
+        private const string MessageFormat = "Override of '{0}()' on type '{1}' must call base.{0}().";
+        private const string Description = "Subclasses of TokenStream that override End(), Reset(), or Close() must call the corresponding base method, otherwise some internal state will not be correctly reset.";
+
+        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
+
+        private static readonly HashSet<string> TrackedMethodNames = new HashSet<string> { "End", "Reset", "Close" };
+
+        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+        public override void Initialize(AnalysisContext context)
+        {
+            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+            context.EnableConcurrentExecution();
+            context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.MethodDeclaration);
+        }
+
+        private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+        {
+            var methodDeclaration = (MethodDeclarationSyntax)context.Node;
+
+            var methodName = methodDeclaration.Identifier.ValueText;
+            if (!TrackedMethodNames.Contains(methodName))
+            {
+                return;
+            }
+
+            // Must be parameterless to match the TokenStream contract methods.
+            if (methodDeclaration.ParameterList.Parameters.Count != 0)
+            {
+                return;
+            }
+
+            // Skip non-overrides; abstract overrides have no body to inspect.
+            if (!methodDeclaration.Modifiers.Any(SyntaxKind.OverrideKeyword))
+            {
+                return;
+            }
+            if (methodDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword) ||
+                methodDeclaration.Modifiers.Any(SyntaxKind.ExternKeyword))
+            {
+                return;
+            }
+
+            var classDeclaration = methodDeclaration.Parent as ClassDeclarationSyntax;
+            if (classDeclaration is null)
+            {
+                return;
+            }
+
+            var classTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol;
+            if (!InheritsFrom(classTypeSymbol, "Lucene.Net.Analysis.TokenStream"))
+            {
+                return;
+            }
+
+            if (ContainsBaseCall(methodDeclaration, methodName))
+            {
+                return;
+            }
+
+            context.ReportDiagnostic(Diagnostic.Create(Rule, methodDeclaration.Identifier.GetLocation(), methodName, classDeclaration.Identifier.ValueText));
+        }
+
+        private static bool ContainsBaseCall(MethodDeclarationSyntax methodDeclaration, string methodName)
+        {
+            // Inspect either a block body or an expression-bodied member.
+            SyntaxNode body;
+            IEnumerable<SyntaxNode> nodes;
+            if (methodDeclaration.Body is not null)
+            {
+                body = methodDeclaration.Body;
+                nodes = methodDeclaration.Body.DescendantNodes();
+            }
+            else if (methodDeclaration.ExpressionBody is not null)
+            {
+                body = methodDeclaration.ExpressionBody;
+                nodes = methodDeclaration.ExpressionBody.DescendantNodesAndSelf();
+            }
+            else
+            {
+                // No body (e.g. partial without implementation) — nothing to check.
+                return true;
+            }
+
+            foreach (var node in nodes)
+            {
+                if (node is InvocationExpressionSyntax invocation &&
+                    invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+                    memberAccess.Expression.IsKind(SyntaxKind.BaseExpression) &&
+                    memberAccess.Name.Identifier.ValueText == methodName &&
+                    !IsInsideNestedFunction(invocation, body))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // A base call inside a lambda, anonymous method, or local function does not necessarily
+        // execute when the enclosing method runs (the delegate may never be invoked). Treat such
+        // occurrences as not satisfying the contract.
+        private static bool IsInsideNestedFunction(SyntaxNode node, SyntaxNode body)
+        {
+            for (var ancestor = node.Parent; ancestor is not null && ancestor != body; ancestor = ancestor.Parent)
+            {
+                if (ancestor is LambdaExpressionSyntax ||
+                    ancestor is AnonymousMethodExpressionSyntax ||
+                    ancestor is LocalFunctionStatementSyntax)
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private static bool InheritsFrom(ITypeSymbol symbol, string expectedParentTypeName)
+        {
+            while (symbol != null)
+            {
+                if (symbol.ToString().Equals(expectedParentTypeName))
+                {
+                    return true;
+                }
+                symbol = symbol.BaseType;
+            }
+            return false;
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1002_AddEndOverrideCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1002_AddEndOverrideCSCodeFixProvider.cs
new file mode 100644
index 000000000..e7c4f6add
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1002_AddEndOverrideCSCodeFixProvider.cs
@@ -0,0 +1,111 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Lucene1002_AddEndOverrideCSCodeFixProvider)), Shared]
+    public class Lucene1002_AddEndOverrideCSCodeFixProvider : CodeFixProvider
+    {
+        private const string Title = "Add End() override that calls base.End()";
+
+        public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(Lucene1002_TokenizerMustOverrideEndCSAnalyzer.DiagnosticId);
+
+        public sealed override FixAllProvider GetFixAllProvider()
+        {
+            return WellKnownFixAllProviders.BatchFixer;
+        }
+
+        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+        {
+            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+            var diagnostic = context.Diagnostics.First();
+            var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+            var classDeclaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().FirstOrDefault();
+            if (classDeclaration is null)
+            {
+                return;
+            }
+
+            context.RegisterCodeFix(
+                CodeAction.Create(
+                    title: Title,
+                    createChangedDocument: c => AddEndOverrideAsync(context.Document, classDeclaration, c),
+                    equivalenceKey: Title),
+                diagnostic);
+        }
+
+        private static async Task<Document> AddEndOverrideAsync(Document document, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
+        {
+            // public override void End()
+            // {
+            //     base.End();
+            //     // TODO: set the final offset and finish up other end-of-stream attributes
+            // }
+            var baseInvocation = SyntaxFactory.ExpressionStatement(
+                SyntaxFactory.InvocationExpression(
+                    SyntaxFactory.MemberAccessExpression(
+                        SyntaxKind.SimpleMemberAccessExpression,
+                        SyntaxFactory.BaseExpression(),
+                        SyntaxFactory.IdentifierName("End"))));
+
+            var todoComment = SyntaxFactory.Comment("// TODO: set the final offset and finish up other end-of-stream attributes");
+            var closeBrace = SyntaxFactory.Token(SyntaxKind.CloseBraceToken)
+                .WithLeadingTrivia(SyntaxFactory.TriviaList(todoComment, SyntaxFactory.ElasticCarriageReturnLineFeed));
+
+            var body = SyntaxFactory.Block(
+                SyntaxFactory.Token(SyntaxKind.OpenBraceToken),
+                SyntaxFactory.SingletonList<StatementSyntax>(baseInvocation),
+                closeBrace);
+
+            var endMethod = SyntaxFactory.MethodDeclaration(
+                    attributeLists: default,
+                    modifiers: SyntaxFactory.TokenList(
+                        SyntaxFactory.Token(SyntaxKind.PublicKeyword),
+                        SyntaxFactory.Token(SyntaxKind.OverrideKeyword)),
+                    returnType: SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)),
+                    explicitInterfaceSpecifier: null,
+                    identifier: SyntaxFactory.Identifier("End"),
+                    typeParameterList: null,
+                    parameterList: SyntaxFactory.ParameterList(),
+                    constraintClauses: default,
+                    body: body,
+                    expressionBody: null,
+                    semicolonToken: default)
+                .WithAdditionalAnnotations(Formatter.Annotation);
+
+            var newClassDeclaration = classDeclaration.AddMembers(endMethod);
+
+            var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+            var newRoot = oldRoot.ReplaceNode(classDeclaration, newClassDeclaration);
+            return document.WithSyntaxRoot(newRoot);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1002_TokenizerMustOverrideEndCSAnalyzer.cs b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1002_TokenizerMustOverrideEndCSAnalyzer.cs
new file mode 100644
index 000000000..51391efec
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.CSharp/Lucene1002_TokenizerMustOverrideEndCSAnalyzer.cs
@@ -0,0 +1,119 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    // LUCENENET: The Analysis package documentation requires that Tokenizer subclasses
+    // override End() to set a correct final offset and finish other end-of-stream attributes.
+    // See "More Requirements for Analysis Component Classes" / "Tokenizer" in
+    // src/Lucene.Net/Analysis/package.md. This analyzer surfaces that requirement at compile time.
+    [DiagnosticAnalyzer(LanguageNames.CSharp)]
+    public class Lucene1002_TokenizerMustOverrideEndCSAnalyzer : DiagnosticAnalyzer
+    {
+        public const string DiagnosticId = "Lucene1002";
+        private const string Category = "Design";
+
+        private const string Title = "Tokenizer subclasses must override End().";
+        private const string MessageFormat = "Tokenizer subclass '{0}' must override End().";
+        private const string Description = "Tokenizer subclasses must override End() to set the correct final offset and finish other end-of-stream attributes.";
+
+        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
+
+        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+        public override void Initialize(AnalysisContext context)
+        {
+            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+            context.EnableConcurrentExecution();
+            context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
+        }
+
+        private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+        {
+            var classDeclaration = (ClassDeclarationSyntax)context.Node;
+
+            // Skip abstract classes — they aren't required to provide concrete End().
+            if (classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
+            {
+                return;
+            }
+
+            var classTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol;
+            if (!InheritsFromTokenizer(classTypeSymbol))
+            {
+                return;
+            }
+
+            // Skip if any class in the hierarchy from the current type up to (but not including)
+            // Tokenizer already overrides End(). The override chain satisfies the contract — if
+            // the override is sealed, subclasses cannot override anyway; if it isn't, leaving it
+            // to the ancestor is a valid pattern.
+            if (HasEndOverrideInHierarchy(classTypeSymbol))
+            {
+                return;
+            }
+
+            context.ReportDiagnostic(Diagnostic.Create(Rule, classDeclaration.Identifier.GetLocation(), classDeclaration.Identifier.ValueText));
+        }
+
+        private static bool InheritsFromTokenizer(INamedTypeSymbol symbol)
+        {
+            if (symbol == null)
+            {
+                return false;
+            }
+            // Strict subclass — the Tokenizer base type itself is abstract and out of scope.
+            var baseType = symbol.BaseType;
+            while (baseType != null)
+            {
+                if (baseType.ToString().Equals("Lucene.Net.Analysis.Tokenizer"))
+                {
+                    return true;
+                }
+                baseType = baseType.BaseType;
+            }
+            return false;
+        }
+
+        private static bool HasEndOverrideInHierarchy(INamedTypeSymbol symbol)
+        {
+            // Walk from the current type up to Tokenizer (exclusive) looking for an End() override.
+            var current = symbol;
+            while (current != null && current.ToString() != "Lucene.Net.Analysis.Tokenizer")
+            {
+                foreach (var member in current.GetMembers("End"))
+                {
+                    if (member is IMethodSymbol method &&
+                        method.Parameters.Length == 0 &&
+                        method.IsOverride)
+                    {
+                        return true;
+                    }
+                }
+                current = current.BaseType;
+            }
+            return false;
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1001_AddBaseMethodCallVBCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1001_AddBaseMethodCallVBCodeFixProvider.cs
new file mode 100644
index 000000000..410ee0c58
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1001_AddBaseMethodCallVBCodeFixProvider.cs
@@ -0,0 +1,88 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.VisualBasic;
+using Microsoft.CodeAnalysis.VisualBasic.Syntax;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    [ExportCodeFixProvider(LanguageNames.VisualBasic, Name = nameof(Lucene1001_AddBaseMethodCallVBCodeFixProvider)), Shared]
+    public class Lucene1001_AddBaseMethodCallVBCodeFixProvider : CodeFixProvider
+    {
+        public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.DiagnosticId);
+
+        public sealed override FixAllProvider GetFixAllProvider()
+        {
+            return WellKnownFixAllProviders.BatchFixer;
+        }
+
+        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+        {
+            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+            var diagnostic = context.Diagnostics.First();
+            var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+            var methodBlock = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<MethodBlockSyntax>().FirstOrDefault();
+            if (methodBlock is null)
+            {
+                return;
+            }
+
+            var methodName = ((MethodStatementSyntax)methodBlock.BlockStatement).Identifier.ValueText;
+            var title = $"Add MyBase.{methodName}() call";
+
+            context.RegisterCodeFix(
+                CodeAction.Create(
+                    title: title,
+                    createChangedDocument: c => AddBaseCallAsync(context.Document, methodBlock, c),
+                    equivalenceKey: title),
+                diagnostic);
+        }
+
+        private static async Task<Document> AddBaseCallAsync(Document document, MethodBlockSyntax methodBlock, CancellationToken cancellationToken)
+        {
+            var methodName = ((MethodStatementSyntax)methodBlock.BlockStatement).Identifier.ValueText;
+
+            var baseInvocation = SyntaxFactory.InvocationExpression(
+                SyntaxFactory.SimpleMemberAccessExpression(
+                    SyntaxFactory.MyBaseExpression(),
+                    SyntaxFactory.Token(SyntaxKind.DotToken),
+                    SyntaxFactory.IdentifierName(methodName)),
+                SyntaxFactory.ArgumentList());
+
+            var baseStatement = SyntaxFactory.ExpressionStatement(baseInvocation)
+                .WithAdditionalAnnotations(Formatter.Annotation);
+
+            var newStatements = methodBlock.Statements.Insert(0, baseStatement);
+            var newMethodBlock = methodBlock.WithStatements(newStatements);
+
+            var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+            var newRoot = oldRoot.ReplaceNode(methodBlock, newMethodBlock);
+            return document.WithSyntaxRoot(newRoot);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.cs b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.cs
new file mode 100644
index 000000000..72df5d8fe
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.cs
@@ -0,0 +1,141 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.VisualBasic;
+using Microsoft.CodeAnalysis.VisualBasic.Syntax;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    [DiagnosticAnalyzer(LanguageNames.VisualBasic)]
+    public class Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer : DiagnosticAnalyzer
+    {
+        public const string DiagnosticId = "Lucene1001";
+        private const string Category = "Design";
+
+        private const string Title = "TokenStream override of End()/Reset()/Close() must call the corresponding base method.";
+        private const string MessageFormat = "Override of '{0}()' on type '{1}' must call MyBase.{0}().";
+        private const string Description = "Subclasses of TokenStream that override End(), Reset(), or Close() must call the corresponding base method, otherwise some internal state will not be correctly reset.";
+
+        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
+
+        private static readonly HashSet<string> TrackedMethodNames = new HashSet<string> { "End", "Reset", "Close" };
+
+        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+        public override void Initialize(AnalysisContext context)
+        {
+            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+            context.EnableConcurrentExecution();
+            context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SubBlock, SyntaxKind.FunctionBlock);
+        }
+
+        private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+        {
+            var methodBlock = (MethodBlockSyntax)context.Node;
+            var methodStatement = (MethodStatementSyntax)methodBlock.BlockStatement;
+
+            var methodName = methodStatement.Identifier.ValueText;
+            if (!TrackedMethodNames.Contains(methodName))
+            {
+                return;
+            }
+
+            // Must be parameterless to match the TokenStream contract methods.
+            if (methodStatement.ParameterList != null && methodStatement.ParameterList.Parameters.Count != 0)
+            {
+                return;
+            }
+
+            // Skip non-overrides.
+            if (!methodStatement.Modifiers.Any(SyntaxKind.OverridesKeyword))
+            {
+                return;
+            }
+            if (methodStatement.Modifiers.Any(SyntaxKind.MustOverrideKeyword))
+            {
+                return;
+            }
+
+            var classBlock = methodBlock.Parent as ClassBlockSyntax;
+            if (classBlock is null)
+            {
+                return;
+            }
+
+            var classTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classBlock.ClassStatement) as ITypeSymbol;
+            if (!InheritsFrom(classTypeSymbol, "Lucene.Net.Analysis.TokenStream"))
+            {
+                return;
+            }
+
+            if (ContainsBaseCall(methodBlock, methodName))
+            {
+                return;
+            }
+
+            context.ReportDiagnostic(Diagnostic.Create(Rule, methodStatement.Identifier.GetLocation(), methodName, classBlock.ClassStatement.Identifier.ValueText));
+        }
+
+        private static bool ContainsBaseCall(MethodBlockSyntax methodBlock, string methodName)
+        {
+            foreach (var node in methodBlock.Statements.SelectMany(s => s.DescendantNodesAndSelf()))
+            {
+                if (node is InvocationExpressionSyntax invocation &&
+                    invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+                    memberAccess.Expression.IsKind(SyntaxKind.MyBaseExpression) &&
+                    memberAccess.Name.Identifier.ValueText == methodName &&
+                    !IsInsideLambda(invocation, methodBlock))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // A base call inside a lambda does not necessarily execute when the enclosing method runs
+        // (the delegate may never be invoked). Treat such occurrences as not satisfying the contract.
+        private static bool IsInsideLambda(SyntaxNode node, SyntaxNode body)
+        {
+            for (var ancestor = node.Parent; ancestor is not null && ancestor != body; ancestor = ancestor.Parent)
+            {
+                if (ancestor is LambdaExpressionSyntax)
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private static bool InheritsFrom(ITypeSymbol symbol, string expectedParentTypeName)
+        {
+            while (symbol != null)
+            {
+                if (symbol.ToString().Equals(expectedParentTypeName))
+                {
+                    return true;
+                }
+                symbol = symbol.BaseType;
+            }
+            return false;
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1002_AddEndOverrideVBCodeFixProvider.cs b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1002_AddEndOverrideVBCodeFixProvider.cs
new file mode 100644
index 000000000..183af4a0a
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1002_AddEndOverrideVBCodeFixProvider.cs
@@ -0,0 +1,108 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.VisualBasic;
+using Microsoft.CodeAnalysis.VisualBasic.Syntax;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    [ExportCodeFixProvider(LanguageNames.VisualBasic, Name = nameof(Lucene1002_AddEndOverrideVBCodeFixProvider)), Shared]
+    public class Lucene1002_AddEndOverrideVBCodeFixProvider : CodeFixProvider
+    {
+        private const string Title = "Add End() override that calls MyBase.End()";
+
+        public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(Lucene1002_TokenizerMustOverrideEndVBAnalyzer.DiagnosticId);
+
+        public sealed override FixAllProvider GetFixAllProvider()
+        {
+            return WellKnownFixAllProviders.BatchFixer;
+        }
+
+        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+        {
+            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+            var diagnostic = context.Diagnostics.First();
+            var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+            var classBlock = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ClassBlockSyntax>().FirstOrDefault();
+            if (classBlock is null)
+            {
+                return;
+            }
+
+            context.RegisterCodeFix(
+                CodeAction.Create(
+                    title: Title,
+                    createChangedDocument: c => AddEndOverrideAsync(context.Document, classBlock, c),
+                    equivalenceKey: Title),
+                diagnostic);
+        }
+
+        private static async Task<Document> AddEndOverrideAsync(Document document, ClassBlockSyntax classBlock, CancellationToken cancellationToken)
+        {
+            // Public Overrides Sub End()
+            //     MyBase.End()
+            //     ' TODO: set the final offset and finish up other end-of-stream attributes
+            // End Sub
+            var subStatement = SyntaxFactory.SubStatement(
+                    attributeLists: default,
+                    modifiers: SyntaxFactory.TokenList(
+                        SyntaxFactory.Token(SyntaxKind.PublicKeyword),
+                        SyntaxFactory.Token(SyntaxKind.OverridesKeyword)),
+                    subOrFunctionKeyword: SyntaxFactory.Token(SyntaxKind.SubKeyword),
+                    identifier: SyntaxFactory.Identifier("End"),
+                    typeParameterList: null,
+                    parameterList: SyntaxFactory.ParameterList(),
+                    asClause: null,
+                    handlesClause: null,
+                    implementsClause: null);
+
+            var baseInvocation = SyntaxFactory.ExpressionStatement(
+                SyntaxFactory.InvocationExpression(
+                    SyntaxFactory.SimpleMemberAccessExpression(
+                        SyntaxFactory.MyBaseExpression(),
+                        SyntaxFactory.Token(SyntaxKind.DotToken),
+                        SyntaxFactory.IdentifierName("End")),
+                    SyntaxFactory.ArgumentList()));
+
+            var endSubStatement = SyntaxFactory.EndSubStatement()
+                .WithLeadingTrivia(SyntaxFactory.CommentTrivia("' TODO: set the final offset and finish up other end-of-stream attributes"), SyntaxFactory.ElasticCarriageReturnLineFeed);
+
+            var methodBlock = SyntaxFactory.SubBlock(
+                subStatement,
+                SyntaxFactory.SingletonList<StatementSyntax>(baseInvocation),
+                endSubStatement)
+                .WithAdditionalAnnotations(Formatter.Annotation);
+
+            var newClassBlock = classBlock.AddMembers(methodBlock);
+
+            var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+            var newRoot = oldRoot.ReplaceNode(classBlock, newClassBlock);
+            return document.WithSyntaxRoot(newRoot);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1002_TokenizerMustOverrideEndVBAnalyzer.cs b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1002_TokenizerMustOverrideEndVBAnalyzer.cs
new file mode 100644
index 000000000..f4247e3c7
--- /dev/null
+++ b/src/dotnet/Lucene.Net.CodeAnalysis.VisualBasic/Lucene1002_TokenizerMustOverrideEndVBAnalyzer.cs
@@ -0,0 +1,110 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.VisualBasic;
+using Microsoft.CodeAnalysis.VisualBasic.Syntax;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    [DiagnosticAnalyzer(LanguageNames.VisualBasic)]
+    public class Lucene1002_TokenizerMustOverrideEndVBAnalyzer : DiagnosticAnalyzer
+    {
+        public const string DiagnosticId = "Lucene1002";
+        private const string Category = "Design";
+
+        private const string Title = "Tokenizer subclasses must override End().";
+        private const string MessageFormat = "Tokenizer subclass '{0}' must override End().";
+        private const string Description = "Tokenizer subclasses must override End() to set the correct final offset and finish other end-of-stream attributes.";
+
+        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
+
+        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+        public override void Initialize(AnalysisContext context)
+        {
+            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+            context.EnableConcurrentExecution();
+            context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassBlock);
+        }
+
+        private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+        {
+            var classBlock = (ClassBlockSyntax)context.Node;
+            var classStatement = classBlock.ClassStatement;
+
+            // Skip MustInherit (abstract) classes.
+            if (classStatement.Modifiers.Any(SyntaxKind.MustInheritKeyword))
+            {
+                return;
+            }
+
+            var classTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classStatement) as INamedTypeSymbol;
+            if (!InheritsFromTokenizer(classTypeSymbol))
+            {
+                return;
+            }
+
+            if (HasEndOverrideInHierarchy(classTypeSymbol))
+            {
+                return;
+            }
+
+            context.ReportDiagnostic(Diagnostic.Create(Rule, classStatement.Identifier.GetLocation(), classStatement.Identifier.ValueText));
+        }
+
+        private static bool InheritsFromTokenizer(INamedTypeSymbol symbol)
+        {
+            if (symbol == null)
+            {
+                return false;
+            }
+            var baseType = symbol.BaseType;
+            while (baseType != null)
+            {
+                if (baseType.ToString().Equals("Lucene.Net.Analysis.Tokenizer"))
+                {
+                    return true;
+                }
+                baseType = baseType.BaseType;
+            }
+            return false;
+        }
+
+        private static bool HasEndOverrideInHierarchy(INamedTypeSymbol symbol)
+        {
+            var current = symbol;
+            while (current != null && current.ToString() != "Lucene.Net.Analysis.Tokenizer")
+            {
+                foreach (var member in current.GetMembers("End"))
+                {
+                    if (member is IMethodSymbol method &&
+                        method.Parameters.Length == 0 &&
+                        method.IsOverride)
+                    {
+                        return true;
+                    }
+                }
+                current = current.BaseType;
+            }
+            return false;
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs
index b67e686d0..479912fae 100644
--- a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs
+++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/Helpers/DiagnosticVerifier.Helper.cs
@@ -5,6 +5,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.Immutable;
+using System.IO;
 using System.Linq;
 
 namespace TestHelper
@@ -33,6 +34,40 @@ namespace TestHelper
     public abstract partial class DiagnosticVerifier
     {
         private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
+        // LUCENENET: typeof(object) lives in System.Private.CoreLib and BCL types are forwarded through
+        // System.Runtime; analyzer tests that reference base methods returning void (e.g. base.Reset())
+        // need the runtime reference to resolve System.Void. Try the directory next to corlib first;
+        // fall back to scanning TRUSTED_PLATFORM_ASSEMBLIES for hosts where the file isn't co-located.
+        private static readonly MetadataReference SystemRuntimeReference = ResolveSystemRuntimeReference();
+
+        private static MetadataReference ResolveSystemRuntimeReference()
+        {
+            var corlibDir = Path.GetDirectoryName(typeof(object).Assembly.Location);
+            if (!string.IsNullOrEmpty(corlibDir))
+            {
+                var candidate = Path.Combine(corlibDir, "System.Runtime.dll");
+                if (File.Exists(candidate))
+                {
+                    return MetadataReference.CreateFromFile(candidate);
+                }
+            }
+
+            if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
+            {
+                foreach (var path in tpa.Split(Path.PathSeparator))
+                {
+                    if (Path.GetFileName(path).Equals("System.Runtime.dll", StringComparison.OrdinalIgnoreCase) &&
+                        File.Exists(path))
+                    {
+                        return MetadataReference.CreateFromFile(path);
+                    }
+                }
+            }
+
+            throw new FileNotFoundException(
+                "Unable to locate System.Runtime.dll. Looked next to corlib and in TRUSTED_PLATFORM_ASSEMBLIES.");
+        }
+
         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);
@@ -168,6 +203,7 @@ private static Project CreateProject(string[] sources, string language = Languag
                 .CurrentSolution
                 .AddProject(projectId, TestProjectName, TestProjectName, language)
                 .AddMetadataReference(projectId, CorlibReference)
+                .AddMetadataReference(projectId, SystemRuntimeReference)
                 .AddMetadataReference(projectId, SystemCoreReference)
                 .AddMetadataReference(projectId, CSharpSymbolsReference)
                 .AddMetadataReference(projectId, CodeAnalysisReference)
diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1001_AddBaseMethodCallCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1001_AddBaseMethodCallCSCodeFixProvider.cs
new file mode 100644
index 000000000..06ae73973
--- /dev/null
+++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1001_AddBaseMethodCallCSCodeFixProvider.cs
@@ -0,0 +1,418 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using NUnit.Framework;
+using TestHelper;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    public class TestLucene1001_AddBaseMethodCallCSCodeFixProvider : CodeFixVerifier
+    {
+        protected override CodeFixProvider GetCSharpCodeFixProvider()
+        {
+            return new Lucene1001_AddBaseMethodCallCSCodeFixProvider();
+        }
+
+        protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+        {
+            return new Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer();
+        }
+
+        [Test]
+        public void TestEmptyFile()
+        {
+            VerifyCSharpDiagnostic(@"");
+        }
+
+        [Test]
+        public void TestNonTokenStreamSubclass_NoDiagnostic()
+        {
+            // Override of Reset() on a class that doesn't inherit from TokenStream — out of scope.
+            var test = @"
+namespace MyNamespace
+{
+    abstract class Other
+    {
+        public virtual void Reset() { }
+    }
+
+    sealed class TypeName : Other
+    {
+        public override void Reset()
+        {
+            // No base call, but we don't care about non-TokenStream classes.
+        }
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestResetWithoutBaseCall_DiagnosticAndFix()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            ClearAttributes();
+        }
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call base.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+
+            var fixtest = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            base.Reset();
+            ClearAttributes();
+        }
+    }
+}";
+            VerifyCSharpFix(test, fixtest);
+        }
+
+        [Test]
+        public void TestResetWithBaseCall_NoDiagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            base.Reset();
+        }
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestEndWithoutBaseCall_Diagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void End()
+        {
+            ClearAttributes();
+        }
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'End()' on type 'TypeName' must call base.End().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestCloseWithoutBaseCall_Diagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Close()
+        {
+            // missing base.Close()
+        }
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Close()' on type 'TypeName' must call base.Close().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestExpressionBodiedWithoutBaseCall_Diagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset() => System.Diagnostics.Debug.WriteLine(""hi"");
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call base.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+
+            var fixtest = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            base.Reset();
+            System.Diagnostics.Debug.WriteLine(""hi"");
+        }
+    }
+}";
+            VerifyCSharpFix(test, fixtest);
+        }
+
+        [Test]
+        public void TestExpressionBodiedThrow_Diagnostic_CodeFix()
+        {
+            // Regression: an expression-bodied void method whose body is `=> throw ...;`
+            // must be converted to a block whose trailing statement is a ThrowStatement,
+            // not an ExpressionStatement (which would be invalid C#).
+            var test = @"
+using Lucene.Net.Analysis;
+using System;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset() => throw new NotSupportedException();
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call base.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 11, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+
+            var fixtest = @"
+using Lucene.Net.Analysis;
+using System;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            base.Reset();
+            throw new NotSupportedException();
+        }
+    }
+}";
+            VerifyCSharpFix(test, fixtest);
+        }
+
+        [Test]
+        public void TestExpressionBodiedWithBaseCall_NoDiagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset() => base.Reset();
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestNewMethodNotOverride_NoDiagnostic()
+        {
+            // A new method named Reset that's not an override is not the contract method.
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public void Reset(int x)
+        {
+        }
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestTokenFilterMissingBaseCall_Diagnostic()
+        {
+            // Indirect inheritance via TokenFilter must still trigger.
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenFilter
+    {
+        public TypeName(TokenStream input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            // missing base.Reset()
+        }
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call base.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestBaseCallInsideLambda_Diagnostic()
+        {
+            // A base call inside an uninvoked lambda does not satisfy the contract.
+            var test = @"
+using Lucene.Net.Analysis;
+using System;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            Action a = () => base.Reset();
+        }
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call base.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 11, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestBaseCallInsideLocalFunction_Diagnostic()
+        {
+            // A base call inside an uninvoked local function does not satisfy the contract.
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+
+        public override void Reset()
+        {
+            void Local() => base.Reset();
+        }
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodCSAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call base.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 30) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1001_AddBaseMethodCallVBCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1001_AddBaseMethodCallVBCodeFixProvider.cs
new file mode 100644
index 000000000..01d3a07b8
--- /dev/null
+++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1001_AddBaseMethodCallVBCodeFixProvider.cs
@@ -0,0 +1,177 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using NUnit.Framework;
+using TestHelper;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    public class TestLucene1001_AddBaseMethodCallVBCodeFixProvider : CodeFixVerifier
+    {
+        protected override CodeFixProvider GetBasicCodeFixProvider()
+        {
+            return new Lucene1001_AddBaseMethodCallVBCodeFixProvider();
+        }
+
+        protected override DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
+        {
+            return new Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer();
+        }
+
+        [Test]
+        public void TestEmptyFile()
+        {
+            VerifyBasicDiagnostic(@"");
+        }
+
+        [Test]
+        public void TestResetWithoutBaseCall_DiagnosticAndFix()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits TokenStream
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub Reset()
+            Dim x As Integer = 1
+        End Sub
+    End Class
+End Namespace";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call MyBase.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.vb", 12, 30) }
+            };
+
+            VerifyBasicDiagnostic(test, expected);
+
+            var fixtest = @"
+Imports Lucene.Net.Analysis
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits TokenStream
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub Reset()
+            MyBase.Reset()
+            Dim x As Integer = 1
+        End Sub
+    End Class
+End Namespace";
+            VerifyBasicFix(test, fixtest);
+        }
+
+        [Test]
+        public void TestResetWithBaseCall_NoDiagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits TokenStream
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub Reset()
+            MyBase.Reset()
+        End Sub
+    End Class
+End Namespace";
+            VerifyBasicDiagnostic(test);
+        }
+
+        [Test]
+        public void TestEndWithoutBaseCall_Diagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits TokenStream
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub [End]()
+            ClearAttributes()
+        End Sub
+    End Class
+End Namespace";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.DiagnosticId,
+                Message = "Override of 'End()' on type 'TypeName' must call MyBase.End().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.vb", 12, 30) }
+            };
+
+            VerifyBasicDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestBaseCallInsideLambda_Diagnostic()
+        {
+            // A base call inside an uninvoked lambda does not satisfy the contract.
+            var test = @"
+Imports Lucene.Net.Analysis
+Imports System
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits TokenStream
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub Reset()
+            Dim a As Action = Sub() MyBase.Reset()
+        End Sub
+    End Class
+End Namespace";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1001_TokenStreamOverrideMustCallBaseMethodVBAnalyzer.DiagnosticId,
+                Message = "Override of 'Reset()' on type 'TypeName' must call MyBase.Reset().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.vb", 13, 30) }
+            };
+
+            VerifyBasicDiagnostic(test, expected);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1002_AddEndOverrideCSCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1002_AddEndOverrideCSCodeFixProvider.cs
new file mode 100644
index 000000000..819747bb8
--- /dev/null
+++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1002_AddEndOverrideCSCodeFixProvider.cs
@@ -0,0 +1,218 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using NUnit.Framework;
+using TestHelper;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    public class TestLucene1002_AddEndOverrideCSCodeFixProvider : CodeFixVerifier
+    {
+        protected override CodeFixProvider GetCSharpCodeFixProvider()
+        {
+            return new Lucene1002_AddEndOverrideCSCodeFixProvider();
+        }
+
+        protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+        {
+            return new Lucene1002_TokenizerMustOverrideEndCSAnalyzer();
+        }
+
+        [Test]
+        public void TestEmptyFile()
+        {
+            VerifyCSharpDiagnostic(@"");
+        }
+
+        [Test]
+        public void TestTokenizerMissingEndOverride_Diagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+using System.IO;
+
+namespace MyNamespace
+{
+    sealed class TypeName : Tokenizer
+    {
+        public TypeName(TextReader input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1002_TokenizerMustOverrideEndCSAnalyzer.DiagnosticId,
+                Message = "Tokenizer subclass 'TypeName' must override End().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 7, 18) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestTokenizerWithEndOverride_NoDiagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+using System.IO;
+
+namespace MyNamespace
+{
+    sealed class TypeName : Tokenizer
+    {
+        public TypeName(TextReader input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+
+        public override void End()
+        {
+            base.End();
+        }
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestAbstractTokenizer_NoDiagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+using System.IO;
+
+namespace MyNamespace
+{
+    abstract class FooTokenizer : Tokenizer
+    {
+        protected FooTokenizer(TextReader input) : base(input) { }
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestTokenFilter_NoDiagnostic()
+        {
+            // TokenFilter is out of scope — only Tokenizer is required to override End().
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenFilter
+    {
+        public TypeName(TokenStream input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestPlainTokenStream_NoDiagnostic()
+        {
+            // Plain TokenStream subclasses are out of scope.
+            var test = @"
+using Lucene.Net.Analysis;
+
+namespace MyNamespace
+{
+    sealed class TypeName : TokenStream
+    {
+        public override bool IncrementToken() => false;
+    }
+}";
+            VerifyCSharpDiagnostic(test);
+        }
+
+        [Test]
+        public void TestTokenizerMissingEndOverride_CodeFix()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+using System.IO;
+
+namespace MyNamespace
+{
+    sealed class TypeName : Tokenizer
+    {
+        public TypeName(TextReader input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+    }
+}";
+            var fixtest = @"
+using Lucene.Net.Analysis;
+using System.IO;
+
+namespace MyNamespace
+{
+    sealed class TypeName : Tokenizer
+    {
+        public TypeName(TextReader input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+
+        public override void End()
+        {
+            base.End();
+            // TODO: set the final offset and finish up other end-of-stream attributes
+        }
+    }
+}";
+            VerifyCSharpFix(test, fixtest);
+        }
+
+        [Test]
+        public void TestIndirectTokenizerInheritance_Diagnostic()
+        {
+            var test = @"
+using Lucene.Net.Analysis;
+using System.IO;
+
+namespace MyNamespace
+{
+    abstract class FooTokenizer : Tokenizer
+    {
+        protected FooTokenizer(TextReader input) : base(input) { }
+    }
+
+    sealed class TypeName : FooTokenizer
+    {
+        public TypeName(TextReader input) : base(input) { }
+
+        public override bool IncrementToken() => false;
+    }
+}";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1002_TokenizerMustOverrideEndCSAnalyzer.DiagnosticId,
+                Message = "Tokenizer subclass 'TypeName' must override End().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 18) }
+            };
+
+            VerifyCSharpDiagnostic(test, expected);
+        }
+    }
+}
diff --git a/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1002_AddEndOverrideVBCodeFixProvider.cs b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1002_AddEndOverrideVBCodeFixProvider.cs
new file mode 100644
index 000000000..150fa89da
--- /dev/null
+++ b/src/dotnet/Lucene.Net.Tests.CodeAnalysis/TestLucene1002_AddEndOverrideVBCodeFixProvider.cs
@@ -0,0 +1,227 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using NUnit.Framework;
+using TestHelper;
+
+namespace Lucene.Net.CodeAnalysis
+{
+    /*
+     * 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.
+     */
+
+    public class TestLucene1002_AddEndOverrideVBCodeFixProvider : CodeFixVerifier
+    {
+        protected override CodeFixProvider GetBasicCodeFixProvider()
+        {
+            return new Lucene1002_AddEndOverrideVBCodeFixProvider();
+        }
+
+        protected override DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
+        {
+            return new Lucene1002_TokenizerMustOverrideEndVBAnalyzer();
+        }
+
+        [Test]
+        public void TestEmptyFile()
+        {
+            VerifyBasicDiagnostic(@"");
+        }
+
+        [Test]
+        public void TestTokenizerMissingEndOverride_Diagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+Imports System.IO
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits Tokenizer
+
+        Public Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+    End Class
+End Namespace";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1002_TokenizerMustOverrideEndVBAnalyzer.DiagnosticId,
+                Message = "Tokenizer subclass 'TypeName' must override End().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.vb", 6, 26) }
+            };
+
+            VerifyBasicDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestTokenizerWithEndOverride_NoDiagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+Imports System.IO
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits Tokenizer
+
+        Public Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub [End]()
+            MyBase.End()
+        End Sub
+    End Class
+End Namespace";
+            VerifyBasicDiagnostic(test);
+        }
+
+        [Test]
+        public void TestAbstractTokenizer_NoDiagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+Imports System.IO
+
+Namespace MyNamespace
+    MustInherit Class FooTokenizer
+        Inherits Tokenizer
+
+        Protected Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+    End Class
+End Namespace";
+            VerifyBasicDiagnostic(test);
+        }
+
+        [Test]
+        public void TestTokenizerMissingEndOverride_CodeFix()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+Imports System.IO
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits Tokenizer
+
+        Public Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+    End Class
+End Namespace";
+            var fixtest = @"
+Imports Lucene.Net.Analysis
+Imports System.IO
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits Tokenizer
+
+        Public Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+
+        Public Overrides Sub End()
+            MyBase.End()
+            ' TODO: set the final offset and finish up other end-of-stream attributes
+        End Sub
+    End Class
+End Namespace";
+            VerifyBasicFix(test, fixtest);
+        }
+
+        [Test]
+        public void TestIndirectTokenizerInheritance_Diagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+Imports System.IO
+
+Namespace MyNamespace
+    MustInherit Class FooTokenizer
+        Inherits Tokenizer
+
+        Protected Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+    End Class
+
+    NotInheritable Class TypeName
+        Inherits FooTokenizer
+
+        Public Sub New(input As TextReader)
+            MyBase.New(input)
+        End Sub
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+    End Class
+End Namespace";
+            var expected = new DiagnosticResult
+            {
+                Id = Lucene1002_TokenizerMustOverrideEndVBAnalyzer.DiagnosticId,
+                Message = "Tokenizer subclass 'TypeName' must override End().",
+                Severity = DiagnosticSeverity.Warning,
+                Locations = new[] { new DiagnosticResultLocation("Test0.vb", 14, 26) }
+            };
+
+            VerifyBasicDiagnostic(test, expected);
+        }
+
+        [Test]
+        public void TestTokenFilter_NoDiagnostic()
+        {
+            var test = @"
+Imports Lucene.Net.Analysis
+
+Namespace MyNamespace
+    NotInheritable Class TypeName
+        Inherits TokenFilter
+
+        Public Sub New(input As TokenStream)
+            MyBase.New(input)
+        End Sub
+
+        Public Overrides Function IncrementToken() As Boolean
+            Return False
+        End Function
+    End Class
+End Namespace";
+            VerifyBasicDiagnostic(test);
+        }
+    }
+}