diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1001_FloatingPointFormattingCSCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1001_FloatingPointFormattingCSCodeFixProvider.cs index 7d278ee..c582f5b 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1001_FloatingPointFormattingCSCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1001_FloatingPointFormattingCSCodeFixProvider.cs @@ -1,4 +1,4 @@ -/* +/* * 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. @@ -36,94 +36,239 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes public class LuceneDev1001_FloatingPointFormattingCSCodeFixProvider : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds => - [Descriptors.LuceneDev1001_FloatingPointFormatting.Id]; + [ + Descriptors.LuceneDev1001_FloatingPointFormatting.Id, + Descriptors.LuceneDev1006_FloatingPointFormatting.Id + ]; public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public override async Task RegisterCodeFixesAsync(CodeFixContext context) { - Diagnostic? diagnostic = context.Diagnostics.FirstOrDefault(); - if (diagnostic is null) - return; - SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); if (root is null) return; - // the diagnostic in the analyzer is reported on the member access (e.g. "x.ToString") - // but we need the whole invocation (e.g. "x.ToString(...)"). So find the invocation - // by walking ancestors if needed. - SyntaxNode? node = root.FindNode(diagnostic.Location.SourceSpan); - if (node is null) + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) return; - if (node is not ExpressionSyntax exprNode) + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode? node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + if (node is null) + continue; + + if (diagnostic.Id == Descriptors.LuceneDev1001_FloatingPointFormatting.Id) + { + RegisterExplicitToStringFix(context, semanticModel, diagnostic, node); + } + else if (diagnostic.Id == Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + { + RegisterStringEmbeddingFix(context, semanticModel, diagnostic, node); + } + } + } + + private void RegisterExplicitToStringFix( + CodeFixContext context, + SemanticModel semanticModel, + Diagnostic diagnostic, + SyntaxNode node) + { + if (node is not ExpressionSyntax expression) return; - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - if (semanticModel is null) + if (!TryGetJ2NTypeAndMember(semanticModel, expression, out var j2nTypeName, out var memberAccess)) return; - if (!TryGetJ2NTypeAndMember(semanticModel, exprNode, out var j2nTypeName, out var memberAccess)) + string codeElement = $"J2N.Numerics.{j2nTypeName}.ToString(...)"; + + context.RegisterCodeFix( + CodeActionHelper.CreateFromResource( + CodeFixResources.UseX, + c => ReplaceExplicitToStringAsync(context.Document, memberAccess, j2nTypeName, c), + "UseJ2NToString", + codeElement), + diagnostic); + } + + private void RegisterStringEmbeddingFix( + CodeFixContext context, + SemanticModel semanticModel, + Diagnostic diagnostic, + SyntaxNode node) + { + ExpressionSyntax? expression = node as ExpressionSyntax ?? node.AncestorsAndSelf().OfType().FirstOrDefault(); + if (expression is null) + return; + + if (!TryGetFloatingPointTypeName(semanticModel.GetTypeInfo(expression, context.CancellationToken), out var j2nTypeName)) return; string codeElement = $"J2N.Numerics.{j2nTypeName}.ToString(...)"; + InterpolationSyntax? interpolation = expression.AncestorsAndSelf().OfType().FirstOrDefault(); + if (interpolation is not null) + { + context.RegisterCodeFix( + CodeActionHelper.CreateFromResource( + CodeFixResources.UseX, + c => ReplaceInterpolationExpressionAsync(context.Document, interpolation, expression, j2nTypeName, c), + "UseJ2NToString", + codeElement), + diagnostic); + + return; + } + context.RegisterCodeFix( CodeActionHelper.CreateFromResource( CodeFixResources.UseX, - c => ReplaceWithJ2NToStringAsync(context.Document, memberAccess, c), + c => ReplaceConcatenationExpressionAsync(context.Document, expression, j2nTypeName, c), "UseJ2NToString", codeElement), diagnostic); } - private async Task ReplaceWithJ2NToStringAsync( + private async Task ReplaceExplicitToStringAsync( Document document, MemberAccessExpressionSyntax memberAccess, + string j2nTypeName, CancellationToken cancellationToken) { - SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (semanticModel is null) + if (memberAccess.Parent is not InvocationExpressionSyntax invocation) return document; - if (!TryGetJ2NTypeAndMember(semanticModel, memberAccess, out var j2nTypeName, out _)) - return document; + var newArguments = new List + { + SyntaxFactory.Argument(memberAccess.Expression.WithoutTrivia()) + }; + + if (invocation.ArgumentList is not null) + newArguments.AddRange(invocation.ArgumentList.Arguments); + + InvocationExpressionSyntax replacement = CreateJ2NToStringInvocation(j2nTypeName, newArguments) + .WithTriviaFrom(invocation); + + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(invocation, replacement); + + return editor.GetChangedDocument(); + } + + private async Task ReplaceConcatenationExpressionAsync( + Document document, + ExpressionSyntax expression, + string j2nTypeName, + CancellationToken cancellationToken) + { + var arguments = new List + { + SyntaxFactory.Argument(expression.WithoutTrivia()) + }; + + InvocationExpressionSyntax replacement = CreateJ2NToStringInvocation(j2nTypeName, arguments) + .WithLeadingTrivia(expression.GetLeadingTrivia()) + .WithTrailingTrivia(expression.GetTrailingTrivia()); - // Build J2N.Numerics.Single/Double.ToString - MemberAccessExpressionSyntax j2nToStringAccess = SyntaxFactory.MemberAccessExpression( + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(expression, replacement); + + return editor.GetChangedDocument(); + } + + private async Task ReplaceInterpolationExpressionAsync( + Document document, + InterpolationSyntax interpolation, + ExpressionSyntax expression, + string j2nTypeName, + CancellationToken cancellationToken) + { + var arguments = new List + { + SyntaxFactory.Argument(expression.WithoutTrivia()) + }; + + var updatedInterpolation = interpolation; + var alignmentClause = interpolation.AlignmentClause; + + if (interpolation.FormatClause is not null) + { + var formatToken = interpolation.FormatClause.FormatStringToken; + var formatLiteral = SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(formatToken.ValueText)); + arguments.Add(SyntaxFactory.Argument(formatLiteral)); + updatedInterpolation = updatedInterpolation.WithFormatClause(null); + } + + InvocationExpressionSyntax replacementExpression = CreateJ2NToStringInvocation(j2nTypeName, arguments) + .WithLeadingTrivia(expression.GetLeadingTrivia()) + .WithTrailingTrivia(expression.GetTrailingTrivia()); + + updatedInterpolation = updatedInterpolation.WithExpression(replacementExpression); + + if (alignmentClause is not null) + { + updatedInterpolation = updatedInterpolation.WithAlignmentClause(alignmentClause); + } + + updatedInterpolation = updatedInterpolation.WithAdditionalAnnotations(Formatter.Annotation); + + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(interpolation, updatedInterpolation); + + return editor.GetChangedDocument(); + } + + private static InvocationExpressionSyntax CreateJ2NToStringInvocation( + string j2nTypeName, + IEnumerable arguments) + { + MemberAccessExpressionSyntax j2nTypeAccess = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("J2N"), SyntaxFactory.IdentifierName("Numerics")), - SyntaxFactory.IdentifierName(j2nTypeName)) - .WithAdditionalAnnotations(Formatter.Annotation); + SyntaxFactory.IdentifierName(j2nTypeName)); - MemberAccessExpressionSyntax fullAccess = SyntaxFactory.MemberAccessExpression( + MemberAccessExpressionSyntax toStringAccess = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - j2nToStringAccess, + j2nTypeAccess, SyntaxFactory.IdentifierName("ToString")); - // Build invocation: J2N.Numerics..ToString(, ) - if (memberAccess.Parent is not InvocationExpressionSyntax invocation) - return document; + return SyntaxFactory.InvocationExpression( + toStringAccess, + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))) + .WithAdditionalAnnotations(Formatter.Annotation); + } - var newArgs = new List { SyntaxFactory.Argument(memberAccess.Expression) }; - if (invocation.ArgumentList != null) - newArgs.AddRange(invocation.ArgumentList.Arguments); - InvocationExpressionSyntax newInvocation = SyntaxFactory.InvocationExpression( - fullAccess, - SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(newArgs))) - .WithTriviaFrom(invocation) // safe now - .WithAdditionalAnnotations(Formatter.Annotation); + private static bool TryGetFloatingPointTypeName(TypeInfo typeInfo, out string typeName) + { + if (TryGetFloatingPointTypeName(typeInfo.Type, out typeName)) + return true; - DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(memberAccess.Parent, newInvocation); + if (TryGetFloatingPointTypeName(typeInfo.ConvertedType, out typeName)) + return true; - return editor.GetChangedDocument(); + typeName = null!; + return false; + } + + private static bool TryGetFloatingPointTypeName(ITypeSymbol? typeSymbol, out string typeName) + { + typeName = typeSymbol?.SpecialType switch + { + SpecialType.System_Single => "Single", + SpecialType.System_Double => "Double", + _ => null! + }; + + return typeName is not null; } private static bool TryGetJ2NTypeAndMember( @@ -137,21 +282,11 @@ private static bool TryGetJ2NTypeAndMember( if (memberAccess is null) { - j2nTypeName = null!; // we always return false when the value is null, so we can ignore it here. + j2nTypeName = null!; return false; } - var typeInfo = semanticModel.GetTypeInfo(memberAccess.Expression); - var type = typeInfo.Type; - - j2nTypeName = type?.SpecialType switch - { - SpecialType.System_Single => "Single", - SpecialType.System_Double => "Double", - _ => null! // we always return false when the value is null, so we can ignore it here. - }; - - if (j2nTypeName is null) + if (!TryGetFloatingPointTypeName(semanticModel.GetTypeInfo(memberAccess.Expression), out j2nTypeName)) return false; return true; diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1006Sample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1006Sample.cs new file mode 100644 index 0000000..2bc6be7 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1006Sample.cs @@ -0,0 +1,36 @@ +/* + * 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. + */ + +namespace Lucene.Net.CodeAnalysis.Dev.Sample; + +public class LuceneDev1006Sample +{ + private readonly float levelBottom = 1f; + private readonly double maxLevel = 2d; + + public string DescribeConcatenation() + { + return " level " + levelBottom + " to " + maxLevel; + } + + public string DescribeInterpolation() + { + return $" level {levelBottom} to {maxLevel}"; + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index f8f5973..c347c6a 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -3,3 +3,4 @@ Rule ID | Category | Severity | Notes ---------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------- LuceneDev1005 | Design | Warning | Types in the Lucene.Net.Support namespace should not be public + LuceneDev1006 | Design | Warning | Floating point values embedded in strings should be formatted with J2N.Numerics Single or Double ToString methods diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer.cs new file mode 100644 index 0000000..46c22b9 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer.cs @@ -0,0 +1,126 @@ +/* + * 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.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; + +namespace Lucene.Net.CodeAnalysis.Dev +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => [Descriptors.LuceneDev1006_FloatingPointFormatting]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeAddExpression, SyntaxKind.AddExpression); + context.RegisterSyntaxNodeAction(AnalyzeInterpolatedStringExpression, SyntaxKind.InterpolatedStringExpression); + } + + private static void AnalyzeAddExpression(SyntaxNodeAnalysisContext context) + { + if (context.Node is not BinaryExpressionSyntax addExpression) + return; + + if (!IsStringConcatenation(addExpression, context.SemanticModel, context.CancellationToken)) + return; + + if (addExpression.Parent is BinaryExpressionSyntax parent && + parent.IsKind(SyntaxKind.AddExpression) && + IsStringConcatenation(parent, context.SemanticModel, context.CancellationToken)) + { + // Only analyze the outermost concatenation expression to avoid duplicate diagnostics. + return; + } + + foreach (var operand in FlattenConcatenation(addExpression)) + { + var expression = operand is ParenthesizedExpressionSyntax parenthesized + ? parenthesized.Expression + : operand; + + var typeInfo = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken); + if (!FloatingPoint.IsFloatingPointType(typeInfo)) + continue; + + ReportDiagnostic(context, expression); + } + } + + private static void AnalyzeInterpolatedStringExpression(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InterpolatedStringExpressionSyntax interpolatedString) + return; + + foreach (var interpolation in interpolatedString.Contents.OfType()) + { + if (interpolation.Expression is null) + continue; + + var typeInfo = context.SemanticModel.GetTypeInfo(interpolation.Expression, context.CancellationToken); + if (!FloatingPoint.IsFloatingPointType(typeInfo)) + continue; + + ReportDiagnostic(context, interpolation.Expression); + } + } + + private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, ExpressionSyntax expression) + { + var diagnostic = Diagnostic.Create( + Descriptors.LuceneDev1006_FloatingPointFormatting, + expression.GetLocation(), + expression.ToString()); + + context.ReportDiagnostic(diagnostic); + } + + private static bool IsStringConcatenation(ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken); + return typeInfo.Type?.SpecialType == SpecialType.System_String + || typeInfo.ConvertedType?.SpecialType == SpecialType.System_String; + } + + private static IEnumerable FlattenConcatenation(ExpressionSyntax expression) + { + if (expression is BinaryExpressionSyntax binary && binary.IsKind(SyntaxKind.AddExpression)) + { + foreach (var left in FlattenConcatenation(binary.Left)) + yield return left; + + foreach (var right in FlattenConcatenation(binary.Right)) + yield return right; + } + else + { + yield return expression; + } + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index 8bd0b59..2786230 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -206,4 +206,16 @@ under the License. {0} '{1}' should not have public accessibility in the Support namespace + + Floating point values embedded in strings should use J2N Numerics formatting + The title of the diagnostic. + + + Floating point values should be formatted with J2N.Numerics.Single.ToString() or J2N.Numerics.Double.ToString() before being embedded into strings to ensure consistent behavior across runtimes. + An optional longer localizable description of the diagnostic. + + + '{0}' may fail due to floating point precision issues on .NET Framework and .NET Core prior to version 3.0. Floating point values should be formatted with J2N.Numerics.Single.ToString() or J2N.Numerics.Double.ToString() before being embedded into strings. + The format-able message the diagnostic displays. + diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs index fcf5cb5..7fa199c 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs @@ -70,5 +70,12 @@ public static partial class Descriptors Design, Warning ); + + public static readonly DiagnosticDescriptor LuceneDev1006_FloatingPointFormatting = + Diagnostic( + "LuceneDev1006", + Design, + Warning + ); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/FloatingPoint.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/FloatingPoint.cs index f9fbe97..c960d1e 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/FloatingPoint.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/FloatingPoint.cs @@ -39,6 +39,50 @@ public static bool IsFloatingPointType(SymbolInfo symbolInfo) return false; } + + public static bool IsFloatingPointType(TypeInfo typeInfo) + { + if (IsFloatingPointType(typeInfo.Type)) + return true; + + if (IsFloatingPointType(typeInfo.ConvertedType)) + return true; + + return false; + } + + public static bool IsFloatingPointType(ITypeSymbol? typeSymbol) + { + if (typeSymbol is null) + return false; + + return IsSpecialTypeFloatingPoint(typeSymbol.SpecialType); + } + + public static bool TryGetFloatingPointTypeName(TypeInfo typeInfo, out string typeName) + { + if (TryGetFloatingPointTypeName(typeInfo.Type, out typeName)) + return true; + + if (TryGetFloatingPointTypeName(typeInfo.ConvertedType, out typeName)) + return true; + + typeName = null!; + return false; + } + + public static bool TryGetFloatingPointTypeName(ITypeSymbol? typeSymbol, out string typeName) + { + typeName = typeSymbol?.SpecialType switch + { + SpecialType.System_Single => "Single", + SpecialType.System_Double => "Double", + _ => null! + }; + + return typeName is not null; + } + private static bool IsSpecialTypeFloatingPoint(SpecialType specialType) { return specialType == SpecialType.System_Single || specialType == SpecialType.System_Double; diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeFixProvider.cs new file mode 100644 index 0000000..eb96e85 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeFixProvider.cs @@ -0,0 +1,151 @@ +/* + * 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.TestUtilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes +{ + public class TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeFixProvider + { + [Test] + public async Task TestFix_FloatInConcatenation() + { + var testCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return "" level "" + levelBottom; + } +} +"; + + var fixedCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return "" level "" + J2N.Numerics.Single.ToString(levelBottom); + } +} +"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1006_FloatingPointFormatting) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 16, column: 29); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer(), + () => new LuceneDev1001_FloatingPointFormattingCSCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_DoubleInConcatenation() + { + var testCode = @" +namespace J2N.Numerics +{ + public static class Double + { + public static string ToString(double value) => value.ToString(); + } +} + +public class C +{ + private double maxLevel = 2d; + + public string Message() + { + return "" level "" + maxLevel; + } +} +"; + + var fixedCode = @" +namespace J2N.Numerics +{ + public static class Double + { + public static string ToString(double value) => value.ToString(); + } +} + +public class C +{ + private double maxLevel = 2d; + + public string Message() + { + return "" level "" + J2N.Numerics.Double.ToString(maxLevel); + } +} +"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1006_FloatingPointFormatting) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("maxLevel") + .WithLocation("/0/Test0.cs", line: 16, column: 29); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer(), + () => new LuceneDev1001_FloatingPointFormattingCSCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeFixProvider.cs new file mode 100644 index 0000000..0055615 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeFixProvider.cs @@ -0,0 +1,215 @@ +/* + * 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.TestUtilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes +{ + public class TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeFixProvider + { + [Test] + public async Task TestFix_FloatInInterpolation() + { + var testCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return $"" level {levelBottom}""; + } +} +"; + + var fixedCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return $"" level {J2N.Numerics.Single.ToString(levelBottom)}""; + } +} +"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1006_FloatingPointFormatting) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 16, column: 27); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer(), + () => new LuceneDev1001_FloatingPointFormattingCSCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + + [Test] + public async Task TestFix_WithAlignmentAndFormatClause() + { + var testCode = @"namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + public static string ToString(float value, string format) => value.ToString(format); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return $"" level {levelBottom,5:F2}""; + } +} +"; + + var fixedCode = @"namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + public static string ToString(float value, string format) => value.ToString(format); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return $"" level {J2N.Numerics.Single.ToString(levelBottom, ""F2""),5}""; + } +} +"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1006_FloatingPointFormatting) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 16, column: 27); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer(), + () => new LuceneDev1001_FloatingPointFormattingCSCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_WithFormatClause() + { + var testCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + public static string ToString(float value, string format) => value.ToString(format); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return $"" level {levelBottom:F2}""; + } +} +"; + + var fixedCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + public static string ToString(float value, string format) => value.ToString(format); + } +} + +public class C +{ + private float levelBottom = 1f; + + public string Message() + { + return $"" level {J2N.Numerics.Single.ToString(levelBottom, ""F2"")}""; + } +} +"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1006_FloatingPointFormatting) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 17, column: 27); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer(), + () => new LuceneDev1001_FloatingPointFormattingCSCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer.cs new file mode 100644 index 0000000..cdba3b7 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer.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.TestUtilities; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; + +namespace Lucene.Net.CodeAnalysis.Dev +{ + public class TestLuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer + { + [Test] + public async Task TestDiagnostic_FloatValuesInConcatenation() + { + var testCode = @" +public class C +{ + private float levelBottom = 1f; + private float maxLevel = 2f; + + private string Message(string value) => value; + + public void M(int upto, int start) + { + Message("" level "" + levelBottom + "" to "" + maxLevel + "": "" + (1 + upto - start) + "" segments""); + } +} +"; + + var expectedLevelBottom = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 11, column: 30); + + var expectedMaxLevel = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("maxLevel") + .WithLocation("/0/Test0.cs", line: 11, column: 53); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expectedLevelBottom, expectedMaxLevel } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestNoDiagnostic_WhenValuesAlreadyFormatted() + { + var testCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + } + + public static class Double + { + public static string ToString(double value) => value.ToString(); + } +} + +public class C +{ + private float levelBottom = 1f; + private float maxLevel = 2f; + + private string Message(string value) => value; + + public void M() + { + Message("" level "" + J2N.Numerics.Single.ToString(levelBottom) + "" to "" + J2N.Numerics.Single.ToString(maxLevel)); + } +} +"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeAnalyzer.cs new file mode 100644 index 0000000..ff409b7 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeAnalyzer.cs @@ -0,0 +1,161 @@ +/* + * 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.TestUtilities; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; + +namespace Lucene.Net.CodeAnalysis.Dev +{ + public class TestLuceneDev1006_FloatingPointFormattingInterpolationCSCodeAnalyzer + { + [Test] + public async Task TestDiagnostic_FloatValuesInInterpolation() + { + var testCode = @" +public class C +{ + private float levelBottom = 1f; + private double maxLevel = 2d; + + private string Message(string value) => value; + + public void M() + { + Message($"" level {levelBottom} to {maxLevel}""); + } +} +"; + + var expectedLevelBottom = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 11, column: 28); + + var expectedMaxLevel = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("maxLevel") + .WithLocation("/0/Test0.cs", line: 11, column: 45); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expectedLevelBottom, expectedMaxLevel } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_WithFormatClause() + { + var testCode = @" +public class C +{ + private float levelBottom = 1f; + + private string Message(string value) => value; + + public void M() + { + Message($"" level {levelBottom:F2}""); + } +} +"; + + var expected = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 10, column: 28); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_WithAlignmentAndFormatClause() + { + var testCode = @" +public class C +{ + private float levelBottom = 1f; + + private string Message(string value) => value; + + public void M() + { + Message($"" level {levelBottom,5:F2}""); + } +} +"; + + var expected = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1006_FloatingPointFormatting.Id) + .WithMessageFormat(Descriptors.LuceneDev1006_FloatingPointFormatting.MessageFormat) + .WithArguments("levelBottom") + .WithLocation("/0/Test0.cs", line: 10, column: 28); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestNoDiagnostic_WhenValuesAlreadyFormatted() + { + var testCode = @" +namespace J2N.Numerics +{ + public static class Single + { + public static string ToString(float value) => value.ToString(); + public static string ToString(float value, string format) => value.ToString(format); + } +} + +public class C +{ + private float levelBottom = 1f; + + private string Message(string value) => value; + + public void M() + { + Message($"" level {J2N.Numerics.Single.ToString(levelBottom)}""); + } +} +"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1006_FloatingPointFormattingConcatenationCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + } +}