diff --git a/DiagnosticCategoryAndIdRanges.txt b/DiagnosticCategoryAndIdRanges.txt
index e5c2083..3fe99d1 100644
--- a/DiagnosticCategoryAndIdRanges.txt
+++ b/DiagnosticCategoryAndIdRanges.txt
@@ -14,7 +14,7 @@
# DO NOT remove ID ranges already defined or merge this file in git.
#
Design: LuceneDev1000-LuceneDev1008
-Globalization:
+Globalization: LuceneDev2000-LuceneDev2008
Mobility:
Performance:
Security:
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8796964..63a85fb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -43,5 +43,6 @@
+
diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider.cs
new file mode 100644
index 0000000..890f32f
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider.cs
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file for additional information.
+ * The ASF licenses this file under the Apache License, Version 2.0.
+ */
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev2xxx
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider)), Shared]
+ public sealed class LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider : CodeFixProvider
+ {
+ private const string TitleInvariant = "Add CultureInfo.InvariantCulture";
+ private const string TitleCurrent = "Add CultureInfo.CurrentCulture";
+
+ public override ImmutableArray FixableDiagnosticIds =>
+ ImmutableArray.Create(
+ Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider.Id,
+ Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider.Id,
+ Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider.Id,
+ Descriptors.LuceneDev2004_J2NNumericMissingFormatProvider.Id);
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null) return;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var invocation = root.FindToken(diagnostic.Location.SourceSpan.Start)
+ .Parent?
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+ if (invocation is null) continue;
+
+ RegisterFix(context, invocation, "InvariantCulture", TitleInvariant, diagnostic);
+ RegisterFix(context, invocation, "CurrentCulture", TitleCurrent, diagnostic);
+ }
+ }
+
+ private static void RegisterFix(
+ CodeFixContext context,
+ InvocationExpressionSyntax invocation,
+ string cultureMember,
+ string title,
+ Diagnostic diagnostic)
+ {
+ context.RegisterCodeFix(CodeAction.Create(
+ title: title,
+ createChangedDocument: c => AddCultureArgumentAsync(context.Document, invocation, cultureMember, c),
+ equivalenceKey: title),
+ diagnostic);
+ }
+
+ private static async Task AddCultureArgumentAsync(
+ Document document,
+ InvocationExpressionSyntax invocation,
+ string cultureMember,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null) return document;
+
+ var cultureExpr = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("CultureInfo"),
+ SyntaxFactory.IdentifierName(cultureMember));
+ var newArg = SyntaxFactory.Argument(cultureExpr);
+
+ // For TryParse-style methods, the IFormatProvider must come BEFORE the trailing
+ // `out` argument: bool TryParse(string, IFormatProvider, out T). Otherwise append.
+ var args = invocation.ArgumentList.Arguments;
+ int insertAt = args.Count;
+ if (args.Count > 0 && args[args.Count - 1].RefKindKeyword.IsKind(SyntaxKind.OutKeyword))
+ insertAt = args.Count - 1;
+
+ var newArgs = args.Insert(insertAt, newArg);
+ var newInvocation = invocation.WithArgumentList(invocation.ArgumentList.WithArguments(newArgs));
+ var newRoot = EnsureGlobalizationUsing(root).ReplaceNode(invocation, newInvocation);
+ return document.WithSyntaxRoot(newRoot);
+ }
+
+ internal static SyntaxNode EnsureGlobalizationUsing(SyntaxNode root)
+ {
+ if (root is CompilationUnitSyntax compilationUnit)
+ {
+ var hasUsing = compilationUnit.Usings.Any(u => u.Name?.ToString() == "System.Globalization");
+ if (!hasUsing)
+ {
+ // Match the document's existing line ending so the inserted using directive
+ // doesn't mix CRLF and LF (the .gitattributes for this repo enforces CRLF
+ // for *.cs, but local checkouts may differ).
+ var newline = DetectLineEnding(root);
+ var usingDirective = SyntaxFactory.UsingDirective(
+ SyntaxFactory.QualifiedName(
+ SyntaxFactory.IdentifierName("System"),
+ SyntaxFactory.IdentifierName("Globalization")))
+ .WithTrailingTrivia(SyntaxFactory.ElasticEndOfLine(newline));
+ return compilationUnit.AddUsings(usingDirective);
+ }
+ }
+ return root;
+ }
+
+ private static string DetectLineEnding(SyntaxNode root)
+ {
+ var text = root.GetText();
+ if (text.Lines.Count > 1)
+ {
+ var firstLine = text.Lines[0];
+ var endIncludingBreak = text.Lines[1].Start;
+ var breakLength = endIncludingBreak - firstLine.End;
+ if (breakLength == 2) return "\r\n";
+ if (breakLength == 1)
+ {
+ var ch = text[firstLine.End];
+ if (ch == '\r') return "\r";
+ return "\n";
+ }
+ }
+ return "\n";
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider.cs
new file mode 100644
index 0000000..e8f1b8a
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider.cs
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file for additional information.
+ * The ASF licenses this file under the Apache License, Version 2.0.
+ */
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev2xxx
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider)), Shared]
+ public sealed class LuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider : CodeFixProvider
+ {
+ private const string TitleInvariant = "Add CultureInfo.InvariantCulture";
+ private const string TitleCurrent = "Add CultureInfo.CurrentCulture";
+
+ public override ImmutableArray FixableDiagnosticIds =>
+ ImmutableArray.Create(Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider.Id);
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null) return;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var invocation = root.FindToken(diagnostic.Location.SourceSpan.Start)
+ .Parent?
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+ if (invocation is null) continue;
+
+ Register(context, invocation, "InvariantCulture", TitleInvariant, diagnostic);
+ Register(context, invocation, "CurrentCulture", TitleCurrent, diagnostic);
+ }
+ }
+
+ private static void Register(
+ CodeFixContext context,
+ InvocationExpressionSyntax invocation,
+ string cultureMember,
+ string title,
+ Diagnostic diagnostic)
+ {
+ context.RegisterCodeFix(CodeAction.Create(
+ title: title,
+ createChangedDocument: c => InsertProviderAsync(context.Document, invocation, cultureMember, c),
+ equivalenceKey: title),
+ diagnostic);
+ }
+
+ private static async Task InsertProviderAsync(
+ Document document,
+ InvocationExpressionSyntax invocation,
+ string cultureMember,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null) return document;
+
+ var cultureExpr = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("CultureInfo"),
+ SyntaxFactory.IdentifierName(cultureMember));
+ var providerArg = SyntaxFactory.Argument(cultureExpr);
+
+ // Insert as the new first argument; existing args shift right.
+ var newArgs = invocation.ArgumentList.Arguments.Insert(0, providerArg);
+ var newArgList = invocation.ArgumentList.WithArguments(newArgs);
+ var newInvocation = invocation.WithArgumentList(newArgList);
+
+ var newRoot = LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider
+ .EnsureGlobalizationUsing(root)
+ .ReplaceNode(invocation, newInvocation);
+ return document.WithSyntaxRoot(newRoot);
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider.cs
new file mode 100644
index 0000000..ac91daf
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev2xxx/LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider.cs
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file for additional information.
+ * The ASF licenses this file under the Apache License, Version 2.0.
+ */
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev2xxx
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider)), Shared]
+ public sealed class LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider : CodeFixProvider
+ {
+ private const string TitleInvariant = "Wrap with .ToString(CultureInfo.InvariantCulture)";
+ private const string TitleCurrent = "Wrap with .ToString(CultureInfo.CurrentCulture)";
+
+ public override ImmutableArray FixableDiagnosticIds =>
+ ImmutableArray.Create(
+ Descriptors.LuceneDev2005_NumericStringConcatenation.Id,
+ Descriptors.LuceneDev2006_NumericStringInterpolation.Id);
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null) return;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
+ var expression = token.Parent?
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault(e => e.Span == diagnostic.Location.SourceSpan);
+
+ if (expression is null) continue;
+
+ Register(context, expression, "InvariantCulture", TitleInvariant, diagnostic);
+ Register(context, expression, "CurrentCulture", TitleCurrent, diagnostic);
+ }
+ }
+
+ private static void Register(
+ CodeFixContext context,
+ ExpressionSyntax expression,
+ string cultureMember,
+ string title,
+ Diagnostic diagnostic)
+ {
+ context.RegisterCodeFix(CodeAction.Create(
+ title: title,
+ createChangedDocument: c => WrapAsync(context.Document, expression, cultureMember, c),
+ equivalenceKey: title),
+ diagnostic);
+ }
+
+ private static async Task WrapAsync(
+ Document document,
+ ExpressionSyntax expression,
+ string cultureMember,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null) return document;
+
+ // Parenthesize complex expressions so we don't accidentally bind .ToString to part of a larger expression.
+ ExpressionSyntax receiver = expression is IdentifierNameSyntax || expression is LiteralExpressionSyntax || expression is InvocationExpressionSyntax || expression is MemberAccessExpressionSyntax
+ ? expression.WithoutTrivia()
+ : SyntaxFactory.ParenthesizedExpression(expression.WithoutTrivia());
+
+ var cultureExpr = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName("CultureInfo"),
+ SyntaxFactory.IdentifierName(cultureMember));
+
+ var toStringCall = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ receiver,
+ SyntaxFactory.IdentifierName("ToString")),
+ SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(cultureExpr))))
+ .WithLeadingTrivia(expression.GetLeadingTrivia())
+ .WithTrailingTrivia(expression.GetTrailingTrivia());
+
+ var newRoot = LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider
+ .EnsureGlobalizationUsing(root)
+ .ReplaceNode(expression, toStringCall);
+ return document.WithSyntaxRoot(newRoot);
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj b/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj
index d29b0fc..f8f2d12 100644
--- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj
+++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj
@@ -56,6 +56,10 @@ under the License.
+
+
+
+
diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev2xxx/LuceneDev2000_2008_NumericCultureSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev2xxx/LuceneDev2000_2008_NumericCultureSample.cs
new file mode 100644
index 0000000..c077ab4
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev2xxx/LuceneDev2000_2008_NumericCultureSample.cs
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+using System;
+using System.Globalization;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev2xxx;
+
+public class LuceneDev2000_2008_NumericCultureSample
+{
+ public void TriggerAll()
+ {
+ // 2000: BCL Parse / TryParse without IFormatProvider
+ var i = int.Parse("1");
+ double.TryParse("1.5", out _);
+ long.Parse("42".AsSpan());
+
+ // 2001: BCL ToString without IFormatProvider
+ var s1 = i.ToString();
+ var s2 = i.ToString("D");
+ Span buffer = stackalloc char[16];
+ i.TryFormat(buffer, out _);
+
+ // 2002: System.Convert without IFormatProvider
+ var c1 = Convert.ToInt32("3");
+ var c2 = Convert.ToString(7);
+
+ // 2003: string.Format without IFormatProvider, with numeric arg
+ var f1 = string.Format("{0}", i);
+
+ // 2005: implicit numeric concatenation
+ var concat = "id=" + i;
+ var concat2 = "" + (i + 1);
+
+ // 2006: implicit numeric interpolation
+ var interp = $"value={i}";
+
+ // 2007: explicit IFormatProvider, but not InvariantCulture
+ var nonInvariant = i.ToString(CultureInfo.CurrentCulture);
+
+ // 2008 (off by default): explicit InvariantCulture
+ var invariant = i.ToString(CultureInfo.InvariantCulture);
+
+ // The following should NOT trigger:
+ // - non-numeric Parse
+ var g = Guid.Parse("00000000-0000-0000-0000-000000000000");
+ // - FormattableString.Invariant interpolation
+ var ok = FormattableString.Invariant($"value={i}");
+ }
+
+ // 2001 exemption: parameterless ToString() inside a ToString() override should NOT trigger.
+ public class WithToStringOverride
+ {
+ public int Value { get; set; }
+
+ public override string ToString()
+ {
+ int x = Value;
+ return x.ToString();
+ }
+ }
+}
+
+public class LuceneDev2004_J2NSample
+{
+ public void TriggerJ2N()
+ {
+ // 2004: J2N numeric methods without IFormatProvider.
+ // J2N.Numerics.Int32 has static ToString(int) and ToString(int, string) without IFormatProvider.
+ var s = J2N.Numerics.Int32.ToString(42);
+ var s2 = J2N.Numerics.Int32.ToString(42, "D");
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md
index 53ac42b..5b384e3 100644
--- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md
+++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md
@@ -10,3 +10,12 @@ LuceneDev6002 | Usage | Error | Invalid StringComparison value in String o
LuceneDev6003 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed
LuceneDev6004 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed
LuceneDev6005 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string
+LuceneDev2000 | Globalization | Warning | Numeric Parse/TryParse without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
+LuceneDev2001 | Globalization | Warning | Numeric ToString/TryFormat without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
+LuceneDev2002 | Globalization | Warning | System.Convert numeric to/from string without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
+LuceneDev2003 | Globalization | Warning | string.Format with numeric argument and no IFormatProvider; pass CultureInfo.InvariantCulture (or CurrentCulture) as the first argument
+LuceneDev2004 | Globalization | Warning | J2N.Numerics.* method without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
+LuceneDev2005 | Globalization | Warning | Numeric value concatenated with string formats using current culture; wrap with .ToString(CultureInfo.InvariantCulture) explicitly
+LuceneDev2006 | Globalization | Warning | Numeric value interpolated into string formats using current culture; use FormattableString.Invariant or wrap with .ToString(CultureInfo.InvariantCulture) explicitly
+LuceneDev2007 | Globalization | Warning | Numeric format/parse passes a non-invariant IFormatProvider; suppress when intentional
+LuceneDev2008 | Globalization | Disabled | Numeric format/parse passes CultureInfo.InvariantCulture (review-sweep aid; default Info severity, disabled by default)
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2000_BclNumericParseAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2000_BclNumericParseAnalyzer.cs
new file mode 100644
index 0000000..ba931c9
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2000_BclNumericParseAnalyzer.cs
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2000_BclNumericParseAnalyzer : DiagnosticAnalyzer
+ {
+ private static readonly ImmutableHashSet TargetMethodNames =
+ ImmutableHashSet.Create("Parse", "TryParse");
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)
+ {
+ var invocation = (InvocationExpressionSyntax)ctx.Node;
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ return;
+
+ var methodName = memberAccess.Name.Identifier.ValueText;
+ if (!TargetMethodNames.Contains(methodName))
+ return;
+
+ var semantic = ctx.SemanticModel;
+ var method = semantic.GetSymbolInfo(memberAccess).Symbol as IMethodSymbol;
+ if (method is null)
+ return;
+
+ // Static Parse/TryParse on a BCL numeric type.
+ if (!method.IsStatic)
+ return;
+
+ var containing = method.ContainingType;
+ if (!NumericTypeHelper.IsBclNumericSpecialType(containing))
+ return;
+
+ if (NumericTypeHelper.HasFormatProviderParameter(method, semantic.Compilation))
+ return;
+
+ var typeName = NumericTypeHelper.GetBclNumericTypeName(containing) ?? containing.Name;
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider,
+ memberAccess.Name.GetLocation(),
+ methodName,
+ typeName));
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2001_BclNumericToStringAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2001_BclNumericToStringAnalyzer.cs
new file mode 100644
index 0000000..4b615ac
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2001_BclNumericToStringAnalyzer.cs
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2001_BclNumericToStringAnalyzer : DiagnosticAnalyzer
+ {
+ private static readonly ImmutableHashSet TargetMethodNames =
+ ImmutableHashSet.Create("ToString", "TryFormat");
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)
+ {
+ var invocation = (InvocationExpressionSyntax)ctx.Node;
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ return;
+
+ var methodName = memberAccess.Name.Identifier.ValueText;
+ if (!TargetMethodNames.Contains(methodName))
+ return;
+
+ var semantic = ctx.SemanticModel;
+ var method = semantic.GetSymbolInfo(memberAccess).Symbol as IMethodSymbol;
+ if (method is null)
+ return;
+
+ // Instance call on a BCL numeric value.
+ if (method.IsStatic)
+ return;
+
+ var containing = method.ContainingType;
+ if (!NumericTypeHelper.IsBclNumericSpecialType(containing))
+ return;
+
+ // Bail only when the call site actually supplies a provider argument; methods like
+ // TryFormat declare an *optional* IFormatProvider parameter that callers often omit.
+ if (NumericTypeHelper.GetFormatProviderArgument(invocation, semantic) is not null)
+ return;
+
+ // Exempt parameterless ToString() inside a class's ToString() override —
+ // there's no IFormatProvider parameter to forward to in that context.
+ if (methodName == "ToString"
+ && method.Parameters.Length == 0
+ && NumericTypeHelper.IsInsideToStringOverride(invocation))
+ {
+ return;
+ }
+
+ var typeName = NumericTypeHelper.GetBclNumericTypeName(containing) ?? containing.Name;
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider,
+ memberAccess.Name.GetLocation(),
+ methodName,
+ typeName));
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2002_ConvertNumericAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2002_ConvertNumericAnalyzer.cs
new file mode 100644
index 0000000..9e41248
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2002_ConvertNumericAnalyzer.cs
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using System.Linq;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2002_ConvertNumericAnalyzer : DiagnosticAnalyzer
+ {
+ // Conversions between numeric types and string. Other Convert.* methods aren't culture-sensitive.
+ private static readonly ImmutableHashSet StringToNumberMethods =
+ ImmutableHashSet.Create(
+ "ToByte", "ToSByte",
+ "ToInt16", "ToUInt16",
+ "ToInt32", "ToUInt32",
+ "ToInt64", "ToUInt64",
+ "ToSingle", "ToDouble", "ToDecimal");
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)
+ {
+ var invocation = (InvocationExpressionSyntax)ctx.Node;
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ return;
+
+ var methodName = memberAccess.Name.Identifier.ValueText;
+ bool isToString = methodName == "ToString";
+ if (!isToString && !StringToNumberMethods.Contains(methodName))
+ return;
+
+ var semantic = ctx.SemanticModel;
+ var method = semantic.GetSymbolInfo(memberAccess).Symbol as IMethodSymbol;
+ if (method is null) return;
+
+ if (method.ContainingType?.SpecialType != SpecialType.System_Object
+ && method.ContainingType?.ToDisplayString() != "System.Convert")
+ return;
+
+ if (NumericTypeHelper.HasFormatProviderParameter(method, semantic.Compilation))
+ return;
+
+ if (isToString)
+ {
+ // Only flag Convert.ToString(); skip non-numeric overloads (bool, char, DateTime, object).
+ var firstParamType = method.Parameters.FirstOrDefault()?.Type;
+ if (!NumericTypeHelper.IsBclNumericSpecialType(firstParamType))
+ return;
+ }
+ else
+ {
+ // Only flag Convert.ToXxx(string …); skip overloads that don't take a string source.
+ var firstParam = method.Parameters.FirstOrDefault();
+ if (firstParam is null || firstParam.Type.SpecialType != SpecialType.System_String)
+ return;
+ }
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider,
+ memberAccess.Name.GetLocation(),
+ methodName));
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2003_StringFormatNumericAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2003_StringFormatNumericAnalyzer.cs
new file mode 100644
index 0000000..9d80d7f
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2003_StringFormatNumericAnalyzer.cs
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using System.Linq;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2003_StringFormatNumericAnalyzer : DiagnosticAnalyzer
+ {
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)
+ {
+ var invocation = (InvocationExpressionSyntax)ctx.Node;
+
+ // Match Format(...) called as either `string.Format(...)` (member access) or `Format(...)`
+ // (identifier — e.g. via `using static System.String;`). Containing-type check below
+ // confirms the resolved method really is on System.String.
+ string? methodName = invocation.Expression switch
+ {
+ MemberAccessExpressionSyntax m => m.Name.Identifier.ValueText,
+ IdentifierNameSyntax id => id.Identifier.ValueText,
+ _ => null
+ };
+ if (methodName != "Format")
+ return;
+
+ var semantic = ctx.SemanticModel;
+ var method = semantic.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
+ if (method is null) return;
+
+ if (method.ContainingType?.SpecialType != SpecialType.System_String)
+ return;
+
+ if (NumericTypeHelper.HasFormatProviderParameter(method, semantic.Compilation))
+ return;
+
+ // Only flag if at least one argument is a numeric type. Skip the format string itself (first arg).
+ bool anyNumeric = invocation.ArgumentList.Arguments.Skip(1).Any(arg =>
+ {
+ var t = semantic.GetTypeInfo(arg.Expression).Type;
+ return NumericTypeHelper.IsBclNumericSpecialType(t)
+ || NumericTypeHelper.IsJ2NNumericType(t, semantic.Compilation);
+ });
+ if (!anyNumeric)
+ return;
+
+ var location = invocation.Expression switch
+ {
+ MemberAccessExpressionSyntax m => m.Name.GetLocation(),
+ _ => invocation.Expression.GetLocation()
+ };
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider,
+ location));
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2004_J2NNumericMissingFormatProviderAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2004_J2NNumericMissingFormatProviderAnalyzer.cs
new file mode 100644
index 0000000..c51d3a0
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2004_J2NNumericMissingFormatProviderAnalyzer.cs
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2004_J2NNumericMissingFormatProviderAnalyzer : DiagnosticAnalyzer
+ {
+ private static readonly ImmutableHashSet TargetMethodNames =
+ ImmutableHashSet.Create("Parse", "TryParse", "ToString", "TryFormat");
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2004_J2NNumericMissingFormatProvider);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)
+ {
+ var invocation = (InvocationExpressionSyntax)ctx.Node;
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ return;
+
+ var methodName = memberAccess.Name.Identifier.ValueText;
+ if (!TargetMethodNames.Contains(methodName))
+ return;
+
+ var semantic = ctx.SemanticModel;
+ var method = semantic.GetSymbolInfo(memberAccess).Symbol as IMethodSymbol;
+ if (method is null) return;
+
+ if (!NumericTypeHelper.IsJ2NNumericType(method.ContainingType, semantic.Compilation))
+ return;
+
+ if (NumericTypeHelper.HasFormatProviderParameter(method, semantic.Compilation))
+ return;
+
+ // Exempt parameterless ToString() inside a class's ToString() override (mirrors 2001).
+ if (methodName == "ToString"
+ && method.Parameters.Length == 0
+ && NumericTypeHelper.IsInsideToStringOverride(invocation))
+ {
+ return;
+ }
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2004_J2NNumericMissingFormatProvider,
+ memberAccess.Name.GetLocation(),
+ methodName,
+ method.ContainingType.Name));
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2005_NumericConcatenationAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2005_NumericConcatenationAnalyzer.cs
new file mode 100644
index 0000000..6484ac2
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2005_NumericConcatenationAnalyzer.cs
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2005_NumericConcatenationAnalyzer : DiagnosticAnalyzer
+ {
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2005_NumericStringConcatenation);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeAdd, SyntaxKind.AddExpression);
+ }
+
+ private static void AnalyzeAdd(SyntaxNodeAnalysisContext ctx)
+ {
+ var add = (BinaryExpressionSyntax)ctx.Node;
+
+ // Only consider the topmost AddExpression of a string-concat chain. Children will be
+ // visited by the framework; we reach into them ourselves to flag every numeric subexpression.
+ if (add.Parent is BinaryExpressionSyntax parentAdd && parentAdd.IsKind(SyntaxKind.AddExpression))
+ return;
+
+ var semantic = ctx.SemanticModel;
+
+ // Confirm this is a string concatenation (result is string).
+ var resultType = semantic.GetTypeInfo(add).Type;
+ if (resultType is null || resultType.SpecialType != SpecialType.System_String)
+ return;
+
+ VisitOperand(add, ctx);
+ }
+
+ private static void VisitOperand(ExpressionSyntax node, SyntaxNodeAnalysisContext ctx)
+ {
+ // Walk through nested AddExpressions; flag numeric leaves.
+ if (node is BinaryExpressionSyntax bin && bin.IsKind(SyntaxKind.AddExpression))
+ {
+ VisitOperand(bin.Left, ctx);
+ VisitOperand(bin.Right, ctx);
+ return;
+ }
+
+ // Unwrap parentheses to inspect the underlying expression's type, but flag the outer
+ // expression so trailing-trivia/parens land in the diagnostic span.
+ var inner = node;
+ while (inner is ParenthesizedExpressionSyntax paren)
+ inner = paren.Expression;
+
+ var semantic = ctx.SemanticModel;
+ var typeInfo = semantic.GetTypeInfo(inner);
+ var type = typeInfo.Type;
+
+ if (!NumericTypeHelper.IsBclNumericSpecialType(type)
+ && !NumericTypeHelper.IsJ2NNumericType(type, semantic.Compilation))
+ {
+ return;
+ }
+
+ var typeName = NumericTypeHelper.GetBclNumericTypeName(type)
+ ?? type!.Name;
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2005_NumericStringConcatenation,
+ node.GetLocation(),
+ typeName));
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2006_NumericInterpolationAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2006_NumericInterpolationAnalyzer.cs
new file mode 100644
index 0000000..a046f45
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2006_NumericInterpolationAnalyzer.cs
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2006_NumericInterpolationAnalyzer : DiagnosticAnalyzer
+ {
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(Descriptors.LuceneDev2006_NumericStringInterpolation);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInterpolatedString, SyntaxKind.InterpolatedStringExpression);
+ }
+
+ private static void AnalyzeInterpolatedString(SyntaxNodeAnalysisContext ctx)
+ {
+ var interpolated = (InterpolatedStringExpressionSyntax)ctx.Node;
+ var semantic = ctx.SemanticModel;
+
+ if (NumericTypeHelper.IsInsideInvariantInterpolationContext(interpolated, semantic))
+ return;
+
+ foreach (var content in interpolated.Contents)
+ {
+ if (content is not InterpolationSyntax interp)
+ continue;
+
+ var type = semantic.GetTypeInfo(interp.Expression).Type;
+ if (!NumericTypeHelper.IsBclNumericSpecialType(type)
+ && !NumericTypeHelper.IsJ2NNumericType(type, semantic.Compilation))
+ {
+ continue;
+ }
+
+ var typeName = NumericTypeHelper.GetBclNumericTypeName(type) ?? type!.Name;
+
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2006_NumericStringInterpolation,
+ interp.Expression.GetLocation(),
+ typeName));
+ }
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2007_2008_NumericExplicitCultureAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2007_2008_NumericExplicitCultureAnalyzer.cs
new file mode 100644
index 0000000..d3f930b
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev2xxx/LuceneDev2007_2008_NumericExplicitCultureAnalyzer.cs
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Immutable;
+using System.Linq;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class LuceneDev2007_2008_NumericExplicitCultureAnalyzer : DiagnosticAnalyzer
+ {
+ // Methods this analyzer cares about, across BCL numerics, J2N numerics, System.Convert, and string.Format.
+ private static readonly ImmutableHashSet TargetMethodNames =
+ ImmutableHashSet.Create(
+ "Parse", "TryParse", "ToString", "TryFormat", "Format",
+ "ToByte", "ToSByte", "ToInt16", "ToUInt16", "ToInt32", "ToUInt32",
+ "ToInt64", "ToUInt64", "ToSingle", "ToDouble", "ToDecimal");
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(
+ Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider,
+ Descriptors.LuceneDev2008_NumericInvariantFormatProvider);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)
+ {
+ var invocation = (InvocationExpressionSyntax)ctx.Node;
+
+ string? methodName = invocation.Expression switch
+ {
+ MemberAccessExpressionSyntax m => m.Name.Identifier.ValueText,
+ IdentifierNameSyntax id => id.Identifier.ValueText,
+ _ => null
+ };
+ if (methodName is null || !TargetMethodNames.Contains(methodName))
+ return;
+
+ var semantic = ctx.SemanticModel;
+ var method = semantic.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
+ if (method is null) return;
+
+ // Only consider methods that actually accept an IFormatProvider; otherwise 2000-2004 covers it.
+ if (!NumericTypeHelper.HasFormatProviderParameter(method, semantic.Compilation))
+ return;
+
+ // Restrict to numeric scenarios: containing type is BCL numeric, J2N numeric, System.Convert,
+ // or System.String (string.Format with at least one numeric argument).
+ var containing = method.ContainingType;
+ bool isNumericScenario = false;
+
+ if (NumericTypeHelper.IsBclNumericSpecialType(containing)
+ || NumericTypeHelper.IsJ2NNumericType(containing, semantic.Compilation))
+ {
+ isNumericScenario = true;
+ }
+ else if (containing?.ToDisplayString() == "System.Convert")
+ {
+ isNumericScenario = IsConvertNumericMethod(method);
+ }
+ else if (containing?.SpecialType == SpecialType.System_String && methodName == "Format")
+ {
+ isNumericScenario = invocation.ArgumentList.Arguments.Skip(1).Any(arg =>
+ {
+ var t = semantic.GetTypeInfo(arg.Expression).Type;
+ return NumericTypeHelper.IsBclNumericSpecialType(t)
+ || NumericTypeHelper.IsJ2NNumericType(t, semantic.Compilation);
+ });
+ }
+
+ if (!isNumericScenario)
+ return;
+
+ var providerArg = NumericTypeHelper.GetFormatProviderArgument(invocation, semantic);
+ if (providerArg is null)
+ return;
+
+ bool isInvariant = NumericTypeHelper.IsInvariantCultureAccess(providerArg, semantic);
+
+ var location = invocation.Expression switch
+ {
+ MemberAccessExpressionSyntax m => m.Name.GetLocation(),
+ _ => invocation.Expression.GetLocation()
+ };
+
+ if (isInvariant)
+ {
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2008_NumericInvariantFormatProvider,
+ location,
+ methodName));
+ }
+ else
+ {
+ ctx.ReportDiagnostic(Diagnostic.Create(
+ Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider,
+ location,
+ methodName));
+ }
+ }
+
+ private static bool IsConvertNumericMethod(IMethodSymbol method)
+ {
+ var name = method.Name;
+ if (name == "ToString")
+ {
+ var first = method.Parameters.FirstOrDefault();
+ return NumericTypeHelper.IsBclNumericSpecialType(first?.Type);
+ }
+ // ToByte/ToInt32/etc. — match by name.
+ return name is "ToByte" or "ToSByte"
+ or "ToInt16" or "ToUInt16"
+ or "ToInt32" or "ToUInt32"
+ or "ToInt64" or "ToUInt64"
+ or "ToSingle" or "ToDouble" or "ToDecimal";
+ }
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx
index 53dfc08..90216e6 100644
--- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx
+++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx
@@ -307,4 +307,103 @@ under the License.
Call to '{0}' uses a single-character string literal. Use the char overload instead (e.g., 'x' instead of "x").
+
+
+ Numeric Parse/TryParse should specify an IFormatProvider
+
+
+ Calls to numeric Parse and TryParse methods without an IFormatProvider use the current culture in .NET, but Java Lucene parses numerics using invariant culture. Specify CultureInfo.InvariantCulture (or CultureInfo.CurrentCulture, where intended) explicitly.
+
+
+ Call to '{0}' on numeric type '{1}' should specify an IFormatProvider (typically CultureInfo.InvariantCulture).
+
+
+
+
+ Numeric ToString/TryFormat should specify an IFormatProvider
+
+
+ Calls to numeric ToString and TryFormat without an IFormatProvider use the current culture in .NET, but Java Lucene formats numerics using invariant culture. Specify CultureInfo.InvariantCulture (or CultureInfo.CurrentCulture, where intended) explicitly. Calls inside a ToString() override are exempted because that signature has no IFormatProvider parameter.
+
+
+ Call to '{0}' on numeric type '{1}' should specify an IFormatProvider (typically CultureInfo.InvariantCulture).
+
+
+
+
+ System.Convert numeric method should specify an IFormatProvider
+
+
+ Calls to System.Convert numeric methods without an IFormatProvider use the current culture in .NET, but Java Lucene uses invariant culture. Specify CultureInfo.InvariantCulture (or CultureInfo.CurrentCulture, where intended) explicitly. Prefer the Parse/TryParse methods on the type for performance and flexibility.
+
+
+ Call to 'Convert.{0}' should specify an IFormatProvider (typically CultureInfo.InvariantCulture).
+
+
+
+
+ string.Format with numeric argument should specify an IFormatProvider
+
+
+ Calls to string.Format with numeric arguments and no IFormatProvider format the values in the current culture, but Java Lucene uses invariant culture. Pass CultureInfo.InvariantCulture (or CultureInfo.CurrentCulture, where intended) as the first argument.
+
+
+ Call to 'string.Format' with a numeric argument should specify an IFormatProvider (typically CultureInfo.InvariantCulture).
+
+
+
+
+ J2N numeric method should specify an IFormatProvider
+
+
+ Calls to J2N.Numerics.* Parse, TryParse, ToString, or TryFormat methods without an IFormatProvider should be reviewed. J2N intentionally lacks many overloads that omit IFormatProvider; the few that exist (e.g., legacy ToString overloads) should still pass an explicit culture.
+
+
+ Call to '{0}' on J2N numeric type '{1}' should specify an IFormatProvider (typically CultureInfo.InvariantCulture).
+
+
+
+
+ Numeric type concatenated to string formats using current culture
+
+
+ Concatenating a numeric value with a string (e.g., "" + i) implicitly formats the value using the current culture, but Java Lucene uses invariant culture. Wrap the numeric expression in .ToString(CultureInfo.InvariantCulture) (or CultureInfo.CurrentCulture, where intended) explicitly.
+
+
+ Numeric value of type '{0}' is concatenated to a string and will be formatted using the current culture. Wrap with .ToString(CultureInfo.InvariantCulture) explicitly.
+
+
+
+
+ Numeric value in interpolated string formats using current culture
+
+
+ Interpolated strings (e.g., $"{i}") implicitly format numeric values using the current culture, but Java Lucene uses invariant culture. Use FormattableString.Invariant($"...") or string.Create(CultureInfo.InvariantCulture, $"..."), or wrap the value in .ToString(CultureInfo.InvariantCulture) explicitly.
+
+
+ Numeric value of type '{0}' is interpolated into a string and will be formatted using the current culture. Use FormattableString.Invariant or wrap with .ToString(CultureInfo.InvariantCulture) explicitly.
+
+
+
+
+ Numeric format/parse uses non-invariant culture
+
+
+ The IFormatProvider passed to a numeric format/parse call is not CultureInfo.InvariantCulture. Java Lucene uses invariant culture by default; suppress this warning explicitly when the use of a different culture is intentional.
+
+
+ Call to '{0}' passes a non-invariant IFormatProvider. Use CultureInfo.InvariantCulture unless current-culture behavior is intentional.
+
+
+
+
+ Numeric format/parse uses CultureInfo.InvariantCulture (review aid)
+
+
+ Disabled by default. Enable during the culture-correctness audit to walk every numeric format/parse call that already passes CultureInfo.InvariantCulture, so each can be verified.
+
+
+ Call to '{0}' passes CultureInfo.InvariantCulture; verify this matches Lucene's behavior at this site.
+
+
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev2xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev2xxx.cs
new file mode 100644
index 0000000..5db5e6e
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev2xxx.cs
@@ -0,0 +1,106 @@
+/*
+ * 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,
+ * 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.
+ */
+
+using Microsoft.CodeAnalysis;
+using static Microsoft.CodeAnalysis.DiagnosticSeverity;
+using static Lucene.Net.CodeAnalysis.Dev.Utility.Category;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Utility
+{
+ public static partial class Descriptors
+ {
+ // IMPORTANT: Do not make these into properties!
+ // The AnalyzerReleases release management analyzers do not recognize them
+ // and will report RS2002 warnings if it cannot read the DiagnosticDescriptor
+ // instance through a field.
+
+ // 2000: BCL numeric Parse/TryParse without IFormatProvider
+ public static readonly DiagnosticDescriptor LuceneDev2000_BclNumericParseMissingFormatProvider =
+ Diagnostic(
+ "LuceneDev2000",
+ Globalization,
+ Warning
+ );
+
+ // 2001: BCL numeric ToString/TryFormat without IFormatProvider
+ public static readonly DiagnosticDescriptor LuceneDev2001_BclNumericToStringMissingFormatProvider =
+ Diagnostic(
+ "LuceneDev2001",
+ Globalization,
+ Warning
+ );
+
+ // 2002: System.Convert numeric to/from string without IFormatProvider
+ public static readonly DiagnosticDescriptor LuceneDev2002_ConvertNumericMissingFormatProvider =
+ Diagnostic(
+ "LuceneDev2002",
+ Globalization,
+ Warning
+ );
+
+ // 2003: string.Format without IFormatProvider where any argument is numeric
+ public static readonly DiagnosticDescriptor LuceneDev2003_StringFormatNumericMissingFormatProvider =
+ Diagnostic(
+ "LuceneDev2003",
+ Globalization,
+ Warning
+ );
+
+ // 2004: J2N.Numerics.* member without IFormatProvider
+ public static readonly DiagnosticDescriptor LuceneDev2004_J2NNumericMissingFormatProvider =
+ Diagnostic(
+ "LuceneDev2004",
+ Globalization,
+ Warning
+ );
+
+ // 2005: Implicit numeric formatting via string concatenation
+ public static readonly DiagnosticDescriptor LuceneDev2005_NumericStringConcatenation =
+ Diagnostic(
+ "LuceneDev2005",
+ Globalization,
+ Warning
+ );
+
+ // 2006: Implicit numeric formatting via string interpolation
+ public static readonly DiagnosticDescriptor LuceneDev2006_NumericStringInterpolation =
+ Diagnostic(
+ "LuceneDev2006",
+ Globalization,
+ Warning
+ );
+
+ // 2007: Explicit IFormatProvider passed to numeric API, but it is not InvariantCulture
+ public static readonly DiagnosticDescriptor LuceneDev2007_NumericNonInvariantFormatProvider =
+ Diagnostic(
+ "LuceneDev2007",
+ Globalization,
+ Warning
+ );
+
+ // 2008: Explicit IFormatProvider passed to numeric API, and it IS InvariantCulture
+ // (review-sweep aid; off by default)
+ public static readonly DiagnosticDescriptor LuceneDev2008_NumericInvariantFormatProvider =
+ Diagnostic(
+ "LuceneDev2008",
+ Globalization,
+ Info,
+ isEnabledByDefault: false
+ );
+ }
+}
diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/NumericTypeHelper.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/NumericTypeHelper.cs
new file mode 100644
index 0000000..b1a5e2f
--- /dev/null
+++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/NumericTypeHelper.cs
@@ -0,0 +1,234 @@
+/*
+ * 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.
+ */
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Utility
+{
+ internal static class NumericTypeHelper
+ {
+ // The 11 BCL numeric primitive types covered by the Lucene.NET culture-correctness audit.
+ public static bool IsBclNumericSpecialType(ITypeSymbol? type)
+ {
+ if (type is null) return false;
+ switch (type.SpecialType)
+ {
+ case SpecialType.System_Byte:
+ case SpecialType.System_SByte:
+ case SpecialType.System_Int16:
+ case SpecialType.System_UInt16:
+ case SpecialType.System_Int32:
+ case SpecialType.System_UInt32:
+ case SpecialType.System_Int64:
+ case SpecialType.System_UInt64:
+ case SpecialType.System_Single:
+ case SpecialType.System_Double:
+ case SpecialType.System_Decimal:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Returns the simple numeric type name (e.g. "Int32") for a BCL numeric, or null.
+ public static string? GetBclNumericTypeName(ITypeSymbol? type)
+ {
+ if (type is null) return null;
+ return type.SpecialType switch
+ {
+ SpecialType.System_Byte => "Byte",
+ SpecialType.System_SByte => "SByte",
+ SpecialType.System_Int16 => "Int16",
+ SpecialType.System_UInt16 => "UInt16",
+ SpecialType.System_Int32 => "Int32",
+ SpecialType.System_UInt32 => "UInt32",
+ SpecialType.System_Int64 => "Int64",
+ SpecialType.System_UInt64 => "UInt64",
+ SpecialType.System_Single => "Single",
+ SpecialType.System_Double => "Double",
+ SpecialType.System_Decimal => "Decimal",
+ _ => null
+ };
+ }
+
+ private static readonly string[] J2NNumericMetadataNames = new[]
+ {
+ "J2N.Numerics.Int32",
+ "J2N.Numerics.Int64",
+ "J2N.Numerics.Int16",
+ "J2N.Numerics.Byte",
+ "J2N.Numerics.SByte",
+ "J2N.Numerics.Single",
+ "J2N.Numerics.Double",
+ };
+
+ // Resolved J2N numeric types are cached per-Compilation so analyzers don't
+ // re-run GetTypeByMetadataName for every numeric invocation/concat/interpolation node.
+ // ConditionalWeakTable keeps the cache alive only as long as the Compilation is.
+ private sealed class J2NTypeBox { public ImmutableArray Types; }
+ private static readonly ConditionalWeakTable J2NTypeCache = new();
+
+ public static ImmutableArray GetJ2NNumericTypes(Compilation compilation)
+ => J2NTypeCache.GetValue(compilation, ResolveJ2NTypes).Types;
+
+ private static J2NTypeBox ResolveJ2NTypes(Compilation compilation)
+ {
+ var builder = ImmutableArray.CreateBuilder(J2NNumericMetadataNames.Length);
+ foreach (var name in J2NNumericMetadataNames)
+ {
+ var t = compilation.GetTypeByMetadataName(name);
+ if (t is not null) builder.Add(t);
+ }
+ return new J2NTypeBox { Types = builder.ToImmutable() };
+ }
+
+ public static bool IsJ2NNumericType(ITypeSymbol? type, Compilation compilation)
+ {
+ if (type is null) return false;
+ if (type is not INamedTypeSymbol named) return false;
+
+ // Fast pre-filter: skip the symbol-equality loop unless the metadata name matches a J2N type.
+ // (BCL primitives never sit under J2N.Numerics, so this short-circuits the hot path.)
+ if (named.ContainingNamespace?.ToDisplayString() != "J2N.Numerics")
+ return false;
+
+ foreach (var j2n in GetJ2NNumericTypes(compilation))
+ {
+ if (SymbolEqualityComparer.Default.Equals(named, j2n))
+ return true;
+ }
+ return false;
+ }
+
+ public static bool IsNumericType(ITypeSymbol? type, Compilation compilation)
+ => IsBclNumericSpecialType(type) || IsJ2NNumericType(type, compilation);
+
+ // True if any parameter on the method's signature is (or implements) System.IFormatProvider.
+ public static bool HasFormatProviderParameter(IMethodSymbol? method, Compilation compilation)
+ {
+ if (method is null) return false;
+ var fpType = compilation.GetTypeByMetadataName("System.IFormatProvider");
+ if (fpType is null) return false;
+ foreach (var p in method.Parameters)
+ {
+ if (SymbolEqualityComparer.Default.Equals(p.Type, fpType))
+ return true;
+ foreach (var iface in p.Type.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(iface, fpType))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Find the IFormatProvider argument expression in an invocation, if any.
+ public static ExpressionSyntax? GetFormatProviderArgument(
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel)
+ {
+ var fpType = semanticModel.Compilation.GetTypeByMetadataName("System.IFormatProvider");
+ if (fpType is null) return null;
+
+ foreach (var arg in invocation.ArgumentList.Arguments)
+ {
+ // For literals like `null` or `default`, GetTypeInfo(...).Type is null but
+ // ConvertedType reflects the parameter type chosen by overload resolution.
+ var typeInfo = semanticModel.GetTypeInfo(arg.Expression);
+ var argType = typeInfo.Type ?? typeInfo.ConvertedType;
+ if (argType is null) continue;
+ if (SymbolEqualityComparer.Default.Equals(argType, fpType))
+ return arg.Expression;
+ if (argType.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, fpType)))
+ return arg.Expression;
+ }
+ return null;
+ }
+
+ // True when the expression statically resolves to System.Globalization.CultureInfo.InvariantCulture.
+ public static bool IsInvariantCultureAccess(ExpressionSyntax expression, SemanticModel semanticModel)
+ {
+ var symbol = semanticModel.GetSymbolInfo(expression).Symbol;
+ if (symbol is IPropertySymbol prop)
+ {
+ return prop.Name == "InvariantCulture"
+ && prop.ContainingType?.ToDisplayString() == "System.Globalization.CultureInfo";
+ }
+ return false;
+ }
+
+ // True if the given syntax node is lexically inside an `override ToString()` method body.
+ public static bool IsInsideToStringOverride(SyntaxNode node)
+ {
+ for (var current = node.Parent; current is not null; current = current.Parent)
+ {
+ if (current is MethodDeclarationSyntax method
+ && method.Identifier.ValueText == "ToString"
+ && method.ParameterList.Parameters.Count == 0
+ && method.Modifiers.Any(m => m.IsKind(SyntaxKind.OverrideKeyword)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // True when the invocation is the FormattableString argument to FormattableString.Invariant(...)
+ // or string.Create(IFormatProvider, ...) — in those cases an interpolated numeric is fine.
+ public static bool IsInsideInvariantInterpolationContext(SyntaxNode interpolated, SemanticModel semanticModel)
+ {
+ // Walk up to the enclosing invocation that takes this interpolated string as an argument.
+ for (var current = interpolated.Parent; current is not null; current = current.Parent)
+ {
+ if (current is InvocationExpressionSyntax inv)
+ {
+ var symbol = semanticModel.GetSymbolInfo(inv).Symbol as IMethodSymbol;
+ if (symbol is null) continue;
+
+ var containing = symbol.ContainingType?.ToDisplayString();
+ var methodName = symbol.Name;
+
+ // FormattableString.Invariant(FormattableString)
+ if (containing == "System.FormattableString" && methodName == "Invariant")
+ return true;
+
+ // string.Create(IFormatProvider, …) — only treat as invariant when the provider is InvariantCulture.
+ if (containing == "string" || containing == "System.String")
+ {
+ if (methodName == "Create" && inv.ArgumentList.Arguments.Count >= 1)
+ {
+ var first = inv.ArgumentList.Arguments[0].Expression;
+ if (IsInvariantCultureAccess(first, semanticModel))
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.csproj b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.csproj
index 9a30a90..458300b 100644
--- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.csproj
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.csproj
@@ -30,6 +30,7 @@ under the License.
+
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider.cs
new file mode 100644
index 0000000..32d4a07
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider.cs
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider
+ {
+ [Test]
+ public async Task IntParse_AddsInvariantCulture()
+ {
+ var testCode = @"
+public class Sample
+{
+ public int M() => int.Parse(""1"");
+}";
+
+ var fixedCode = @"using System.Globalization;
+
+public class Sample
+{
+ public int M() => int.Parse(""1"", CultureInfo.InvariantCulture);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider.MessageFormat)
+ .WithArguments("Parse", "Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 27);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2000_BclNumericParseAnalyzer(),
+ () => new LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Add CultureInfo.InvariantCulture",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntToString_AddsInvariantCulture()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => i.ToString();
+}";
+
+ var fixedCode = @"using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => i.ToString(CultureInfo.InvariantCulture);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider.MessageFormat)
+ .WithArguments("ToString", "Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 33);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2001_BclNumericToStringAnalyzer(),
+ () => new LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Add CultureInfo.InvariantCulture",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntParse_PreservesCrlfLineEndings()
+ {
+ // Repo policy is `*.cs eol=crlf`. The codefix must emit an inserted `using`
+ // directive with the same line ending as the rest of the document, otherwise
+ // CI (which checks out CRLF) and dev machines (which may not) disagree.
+ var testCode = "\r\npublic class Sample\r\n{\r\n public int M() => int.Parse(\"1\");\r\n}";
+ var fixedCode = "using System.Globalization;\r\n\r\npublic class Sample\r\n{\r\n public int M() => int.Parse(\"1\", CultureInfo.InvariantCulture);\r\n}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider.MessageFormat)
+ .WithArguments("Parse", "Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 27);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2000_BclNumericParseAnalyzer(),
+ () => new LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Add CultureInfo.InvariantCulture",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task DoubleTryParse_InsertsInvariantCultureBeforeOutArg()
+ {
+ // Regression: TryParse signature is (string, IFormatProvider, out T) — the provider
+ // must be inserted BEFORE the trailing `out` parameter, not appended at the end.
+ var testCode = @"
+public class Sample
+{
+ public bool M() => double.TryParse(""1.5"", out _);
+}";
+
+ var fixedCode = @"using System.Globalization;
+
+public class Sample
+{
+ public bool M() => double.TryParse(""1.5"", CultureInfo.InvariantCulture, out _);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider.MessageFormat)
+ .WithArguments("TryParse", "Double")
+ .WithLocation("/0/Test0.cs", line: 4, column: 31);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2000_BclNumericParseAnalyzer(),
+ () => new LuceneDev2000_2001_2002_2004_AddInvariantCultureCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Add CultureInfo.InvariantCulture",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider.cs
new file mode 100644
index 0000000..6ade8fd
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider.cs
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider
+ {
+ [Test]
+ public async Task StringFormat_PrependsInvariantCulture()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => string.Format(""{0}"", i);
+}";
+
+ var fixedCode = @"using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => string.Format(CultureInfo.InvariantCulture, ""{0}"", i);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider.MessageFormat)
+ .WithLocation("/0/Test0.cs", line: 4, column: 38);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2003_StringFormatNumericAnalyzer(),
+ () => new LuceneDev2003_AddInvariantCultureToStringFormatCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Add CultureInfo.InvariantCulture",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider.cs
new file mode 100644
index 0000000..98990ed
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev2xxx/TestLuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider.cs
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider
+ {
+ [Test]
+ public async Task IntPlusString_WrapsWithInvariant()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => ""x="" + i;
+}";
+
+ var fixedCode = @"using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => ""x="" + i.ToString(CultureInfo.InvariantCulture);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2005_NumericStringConcatenation)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2005_NumericStringConcatenation.MessageFormat)
+ .WithArguments("Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 38);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2005_NumericConcatenationAnalyzer(),
+ () => new LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Wrap with .ToString(CultureInfo.InvariantCulture)",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntInInterpolation_WrapsWithInvariant()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => $""value={i}"";
+}";
+
+ var fixedCode = @"using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => $""value={i.ToString(CultureInfo.InvariantCulture)}"";
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2006_NumericStringInterpolation)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2006_NumericStringInterpolation.MessageFormat)
+ .WithArguments("Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 40);
+
+ var test = new InjectableCodeFixTest(
+ () => new LuceneDev2006_NumericInterpolationAnalyzer(),
+ () => new LuceneDev2005_2006_WrapNumericWithInvariantCodeFixProvider())
+ {
+ TestCode = testCode,
+ FixedCode = fixedCode,
+ ExpectedDiagnostics = { expected },
+ CodeActionEquivalenceKey = "Wrap with .ToString(CultureInfo.InvariantCulture)",
+ NumberOfIncrementalIterations = 2,
+ NumberOfFixAllIterations = 2
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/Lucene.Net.CodeAnalysis.Dev.Tests.csproj b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/Lucene.Net.CodeAnalysis.Dev.Tests.csproj
index 69570fd..c06676f 100644
--- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/Lucene.Net.CodeAnalysis.Dev.Tests.csproj
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/Lucene.Net.CodeAnalysis.Dev.Tests.csproj
@@ -30,6 +30,7 @@ under the License.
+
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2000_BclNumericParseAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2000_BclNumericParseAnalyzer.cs
new file mode 100644
index 0000000..885ebe7
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2000_BclNumericParseAnalyzer.cs
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2000_BclNumericParseAnalyzer
+ {
+ [Test]
+ public async Task EmptyFile_NoDiagnostic()
+ {
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2000_BclNumericParseAnalyzer())
+ {
+ TestCode = string.Empty
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntParse_String_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public void M()
+ {
+ var x = int.Parse(""1"");
+ }
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider.MessageFormat)
+ .WithArguments("Parse", "Int32")
+ .WithLocation("/0/Test0.cs", line: 6, column: 21);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2000_BclNumericParseAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task DoubleTryParse_String_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public void M()
+ {
+ double.TryParse(""1.5"", out _);
+ }
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2000_BclNumericParseMissingFormatProvider.MessageFormat)
+ .WithArguments("TryParse", "Double")
+ .WithLocation("/0/Test0.cs", line: 6, column: 16);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2000_BclNumericParseAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntParse_WithProvider_NoDiagnostic()
+ {
+ var testCode = @"
+using System.Globalization;
+
+public class Sample
+{
+ public void M()
+ {
+ var x = int.Parse(""1"", CultureInfo.InvariantCulture);
+ }
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2000_BclNumericParseAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task GuidParse_NoDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public void M()
+ {
+ var g = Guid.Parse(""00000000-0000-0000-0000-000000000000"");
+ }
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2000_BclNumericParseAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2001_BclNumericToStringAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2001_BclNumericToStringAnalyzer.cs
new file mode 100644
index 0000000..6a9d51d
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2001_BclNumericToStringAnalyzer.cs
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2001_BclNumericToStringAnalyzer
+ {
+ [Test]
+ public async Task IntToString_Parameterless_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => i.ToString();
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider.MessageFormat)
+ .WithArguments("ToString", "Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 33);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntToString_FormatStringOnly_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => i.ToString(""D"");
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider.MessageFormat)
+ .WithArguments("ToString", "Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 33);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntTryFormat_WithoutProvider_ReportsDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public bool M(int i, Span buffer) => i.TryFormat(buffer, out _);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2001_BclNumericToStringMissingFormatProvider.MessageFormat)
+ .WithArguments("TryFormat", "Int32")
+ .WithLocation("/0/Test0.cs", line: 6, column: 50);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntTryFormat_WithProvider_NoDiagnostic()
+ {
+ var testCode = @"
+using System;
+using System.Globalization;
+
+public class Sample
+{
+ public bool M(int i, Span buffer) => i.TryFormat(buffer, out _, provider: CultureInfo.InvariantCulture);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntToString_WithProvider_NoDiagnostic()
+ {
+ var testCode = @"
+using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => i.ToString(CultureInfo.InvariantCulture);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IntToString_InsideToStringOverride_NoDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public int Value { get; set; }
+
+ public override string ToString()
+ {
+ return Value.ToString();
+ }
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task EnumToString_NoDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public string M(DayOfWeek d) => d.ToString();
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2001_BclNumericToStringAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2002_ConvertNumericAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2002_ConvertNumericAnalyzer.cs
new file mode 100644
index 0000000..9ca3a5e
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2002_ConvertNumericAnalyzer.cs
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2002_ConvertNumericAnalyzer
+ {
+ [Test]
+ public async Task ConvertToInt32_String_ReportsDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public int M() => Convert.ToInt32(""1"");
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider.MessageFormat)
+ .WithArguments("ToInt32")
+ .WithLocation("/0/Test0.cs", line: 6, column: 31);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2002_ConvertNumericAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task ConvertToString_Numeric_ReportsDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public string M(int i) => Convert.ToString(i);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2002_ConvertNumericMissingFormatProvider.MessageFormat)
+ .WithArguments("ToString")
+ .WithLocation("/0/Test0.cs", line: 6, column: 39);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2002_ConvertNumericAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task ConvertToString_Bool_NoDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public string M(bool b) => Convert.ToString(b);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2002_ConvertNumericAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task ConvertToInt32_WithProvider_NoDiagnostic()
+ {
+ var testCode = @"
+using System;
+using System.Globalization;
+
+public class Sample
+{
+ public int M() => Convert.ToInt32(""1"", CultureInfo.InvariantCulture);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2002_ConvertNumericAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2003_StringFormatNumericAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2003_StringFormatNumericAnalyzer.cs
new file mode 100644
index 0000000..41475a0
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2003_StringFormatNumericAnalyzer.cs
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2003_StringFormatNumericAnalyzer
+ {
+ [Test]
+ public async Task StringFormat_NumericArg_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => string.Format(""{0}"", i);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2003_StringFormatNumericMissingFormatProvider.MessageFormat)
+ .WithLocation("/0/Test0.cs", line: 4, column: 38);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2003_StringFormatNumericAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task StringFormat_StringOnlyArg_NoDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(string s) => string.Format(""{0}"", s);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2003_StringFormatNumericAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task StringFormat_WithProvider_NoDiagnostic()
+ {
+ var testCode = @"
+using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => string.Format(CultureInfo.InvariantCulture, ""{0}"", i);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2003_StringFormatNumericAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2004_J2NNumericMissingFormatProviderAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2004_J2NNumericMissingFormatProviderAnalyzer.cs
new file mode 100644
index 0000000..39d4637
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2004_J2NNumericMissingFormatProviderAnalyzer.cs
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2004_J2NNumericMissingFormatProviderAnalyzer
+ {
+ private static readonly MetadataReference J2NReference =
+ MetadataReference.CreateFromFile(typeof(J2N.Numerics.Int32).Assembly.Location);
+
+ [Test]
+ public async Task J2NInt32ToString_OnValue_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M() => J2N.Numerics.Int32.ToString(42);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2004_J2NNumericMissingFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2004_J2NNumericMissingFormatProvider.MessageFormat)
+ .WithArguments("ToString", "Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 45);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2004_J2NNumericMissingFormatProviderAnalyzer())
+ {
+ TestCode = testCode,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ ExpectedDiagnostics = { expected }
+ };
+ test.TestState.AdditionalReferences.Add(J2NReference);
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task J2NInt32ToString_WithProvider_NoDiagnostic()
+ {
+ var testCode = @"
+using System.Globalization;
+
+public class Sample
+{
+ public string M() => J2N.Numerics.Int32.ToString(42, CultureInfo.InvariantCulture);
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2004_J2NNumericMissingFormatProviderAnalyzer())
+ {
+ TestCode = testCode,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80
+ };
+ test.TestState.AdditionalReferences.Add(J2NReference);
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2005_NumericConcatenationAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2005_NumericConcatenationAnalyzer.cs
new file mode 100644
index 0000000..3279ecb
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2005_NumericConcatenationAnalyzer.cs
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2005_NumericConcatenationAnalyzer
+ {
+ [Test]
+ public async Task IntPlusString_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => ""x="" + i;
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2005_NumericStringConcatenation)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2005_NumericStringConcatenation.MessageFormat)
+ .WithArguments("Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 38);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2005_NumericConcatenationAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task EmptyStringPlusInt_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int a, int b) => """" + (a + b);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2005_NumericStringConcatenation)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2005_NumericStringConcatenation.MessageFormat)
+ .WithArguments("Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 43);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2005_NumericConcatenationAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task StringPlusString_NoDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(string a, string b) => a + b;
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2005_NumericConcatenationAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2006_NumericInterpolationAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2006_NumericInterpolationAnalyzer.cs
new file mode 100644
index 0000000..e0dd139
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2006_NumericInterpolationAnalyzer.cs
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2006_NumericInterpolationAnalyzer
+ {
+ [Test]
+ public async Task IntInInterpolation_ReportsDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => $""value={i}"";
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2006_NumericStringInterpolation)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2006_NumericStringInterpolation.MessageFormat)
+ .WithArguments("Int32")
+ .WithLocation("/0/Test0.cs", line: 4, column: 40);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2006_NumericInterpolationAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task StringInInterpolation_NoDiagnostic()
+ {
+ var testCode = @"
+public class Sample
+{
+ public string M(string s) => $""value={s}"";
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2006_NumericInterpolationAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task FormattableStringInvariant_NoDiagnostic()
+ {
+ var testCode = @"
+using System;
+
+public class Sample
+{
+ public string M(int i) => FormattableString.Invariant($""value={i}"");
+}";
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2006_NumericInterpolationAnalyzer())
+ {
+ TestCode = testCode
+ };
+ await test.RunAsync();
+ }
+ }
+}
diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2007_2008_NumericExplicitCultureAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2007_2008_NumericExplicitCultureAnalyzer.cs
new file mode 100644
index 0000000..06ff630
--- /dev/null
+++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev2xxx/TestLuceneDev2007_2008_NumericExplicitCultureAnalyzer.cs
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+using Lucene.Net.CodeAnalysis.Dev.LuceneDev2xxx;
+using Lucene.Net.CodeAnalysis.Dev.TestUtilities;
+using Lucene.Net.CodeAnalysis.Dev.Utility;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev2xxx
+{
+ [TestFixture]
+ public class TestLuceneDev2007_2008_NumericExplicitCultureAnalyzer
+ {
+ [Test]
+ public async Task NonInvariantCulture_Reports2007()
+ {
+ var testCode = @"
+using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => i.ToString(CultureInfo.CurrentCulture);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider.MessageFormat)
+ .WithArguments("ToString")
+ .WithLocation("/0/Test0.cs", line: 6, column: 33);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2007_2008_NumericExplicitCultureAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task InvariantCulture_Reports2008WhenEnabled()
+ {
+ // 2008 is disabled by default in production but the analyzer test framework
+ // enables every supported diagnostic of an injected analyzer regardless,
+ // so we verify here that the analyzer raises it on InvariantCulture call sites.
+ var testCode = @"
+using System.Globalization;
+
+public class Sample
+{
+ public string M(int i) => i.ToString(CultureInfo.InvariantCulture);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2008_NumericInvariantFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Info)
+ .WithMessageFormat(Descriptors.LuceneDev2008_NumericInvariantFormatProvider.MessageFormat)
+ .WithArguments("ToString")
+ .WithLocation("/0/Test0.cs", line: 6, column: 33);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2007_2008_NumericExplicitCultureAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task NullProviderLiteral_Reports2007()
+ {
+ // Regression: passing `null` for IFormatProvider should be treated as a non-invariant
+ // explicit provider (it's effectively current-culture). The argument's TypeInfo.Type
+ // is null for the `null` literal, so detection must fall back to ConvertedType.
+ var testCode = @"
+public class Sample
+{
+ public string M(int i) => i.ToString((string)null, null);
+}";
+
+ var expected = new DiagnosticResult(Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider)
+ .WithSeverity(DiagnosticSeverity.Warning)
+ .WithMessageFormat(Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider.MessageFormat)
+ .WithArguments("ToString")
+ .WithLocation("/0/Test0.cs", line: 4, column: 33);
+
+ var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev2007_2008_NumericExplicitCultureAnalyzer())
+ {
+ TestCode = testCode,
+ ExpectedDiagnostics = { expected }
+ };
+ await test.RunAsync();
+ }
+
+ [Test]
+ public async Task IsEnabledByDefault_2007True_2008False()
+ {
+ Assert.That(Descriptors.LuceneDev2007_NumericNonInvariantFormatProvider.IsEnabledByDefault, Is.True);
+ Assert.That(Descriptors.LuceneDev2008_NumericInvariantFormatProvider.IsEnabledByDefault, Is.False);
+ await Task.CompletedTask;
+ }
+ }
+}