Skip to content

Commit 8b84dc2

Browse files
committed
Add analyzer and code fix for floating point string formatting
Introduces LuceneDev1006 analyzer to detect floating point values embedded in strings via concatenation or interpolation, and updates the code fix provider to suggest using J2N.Numerics Single/Double.ToString methods. Adds supporting resources, descriptors, utility methods, sample usage, and comprehensive tests for both analyzer and code fix behaviors.
1 parent 764624a commit 8b84dc2

11 files changed

Lines changed: 946 additions & 53 deletions

src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1001_FloatingPointFormattingCSCodeFixProvider.cs

Lines changed: 182 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Licensed to the Apache Software Foundation (ASF) under one or more
33
* contributor license agreements. See the NOTICE file distributed with
44
* this work for additional information regarding copyright ownership.
@@ -36,94 +36,233 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes
3636
public class LuceneDev1001_FloatingPointFormattingCSCodeFixProvider : CodeFixProvider
3737
{
3838
public override ImmutableArray<string> FixableDiagnosticIds =>
39-
[Descriptors.LuceneDev1001_FloatingPointFormatting.Id];
39+
[
40+
Descriptors.LuceneDev1001_FloatingPointFormatting.Id,
41+
Descriptors.LuceneDev1006_FloatingPointFormatting.Id
42+
];
4043

4144
public override FixAllProvider GetFixAllProvider() =>
4245
WellKnownFixAllProviders.BatchFixer;
4346

4447
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
4548
{
46-
Diagnostic? diagnostic = context.Diagnostics.FirstOrDefault();
47-
if (diagnostic is null)
48-
return;
49-
5049
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
5150
if (root is null)
5251
return;
5352

54-
// the diagnostic in the analyzer is reported on the member access (e.g. "x.ToString")
55-
// but we need the whole invocation (e.g. "x.ToString(...)"). So find the invocation
56-
// by walking ancestors if needed.
57-
SyntaxNode? node = root.FindNode(diagnostic.Location.SourceSpan);
58-
if (node is null)
53+
SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
54+
if (semanticModel is null)
5955
return;
6056

61-
if (node is not ExpressionSyntax exprNode)
57+
foreach (Diagnostic diagnostic in context.Diagnostics)
58+
{
59+
SyntaxNode? node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
60+
if (node is null)
61+
continue;
62+
63+
if (diagnostic.Id == Descriptors.LuceneDev1001_FloatingPointFormatting.Id)
64+
{
65+
RegisterExplicitToStringFix(context, semanticModel, diagnostic, node);
66+
}
67+
else if (diagnostic.Id == Descriptors.LuceneDev1006_FloatingPointFormatting.Id)
68+
{
69+
RegisterStringEmbeddingFix(context, semanticModel, diagnostic, node);
70+
}
71+
}
72+
}
73+
74+
private void RegisterExplicitToStringFix(
75+
CodeFixContext context,
76+
SemanticModel semanticModel,
77+
Diagnostic diagnostic,
78+
SyntaxNode node)
79+
{
80+
if (node is not ExpressionSyntax expression)
6281
return;
6382

64-
SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
65-
if (semanticModel is null)
83+
if (!TryGetJ2NTypeAndMember(semanticModel, expression, out var j2nTypeName, out var memberAccess))
6684
return;
6785

68-
if (!TryGetJ2NTypeAndMember(semanticModel, exprNode, out var j2nTypeName, out var memberAccess))
86+
string codeElement = $"J2N.Numerics.{j2nTypeName}.ToString(...)";
87+
88+
context.RegisterCodeFix(
89+
CodeActionHelper.CreateFromResource(
90+
CodeFixResources.UseX,
91+
c => ReplaceExplicitToStringAsync(context.Document, memberAccess, j2nTypeName, c),
92+
"UseJ2NToString",
93+
codeElement),
94+
diagnostic);
95+
}
96+
97+
private void RegisterStringEmbeddingFix(
98+
CodeFixContext context,
99+
SemanticModel semanticModel,
100+
Diagnostic diagnostic,
101+
SyntaxNode node)
102+
{
103+
ExpressionSyntax? expression = node as ExpressionSyntax ?? node.AncestorsAndSelf().OfType<ExpressionSyntax>().FirstOrDefault();
104+
if (expression is null)
105+
return;
106+
107+
if (!TryGetFloatingPointTypeName(semanticModel.GetTypeInfo(expression, context.CancellationToken), out var j2nTypeName))
69108
return;
70109

71110
string codeElement = $"J2N.Numerics.{j2nTypeName}.ToString(...)";
72111

112+
InterpolationSyntax? interpolation = expression.AncestorsAndSelf().OfType<InterpolationSyntax>().FirstOrDefault();
113+
if (interpolation is not null)
114+
{
115+
context.RegisterCodeFix(
116+
CodeActionHelper.CreateFromResource(
117+
CodeFixResources.UseX,
118+
c => ReplaceInterpolationExpressionAsync(context.Document, interpolation, expression, j2nTypeName, c),
119+
"UseJ2NToString",
120+
codeElement),
121+
diagnostic);
122+
123+
return;
124+
}
125+
73126
context.RegisterCodeFix(
74127
CodeActionHelper.CreateFromResource(
75128
CodeFixResources.UseX,
76-
c => ReplaceWithJ2NToStringAsync(context.Document, memberAccess, c),
129+
c => ReplaceConcatenationExpressionAsync(context.Document, expression, j2nTypeName, c),
77130
"UseJ2NToString",
78131
codeElement),
79132
diagnostic);
80133
}
81134

82-
private async Task<Document> ReplaceWithJ2NToStringAsync(
135+
private async Task<Document> ReplaceExplicitToStringAsync(
83136
Document document,
84137
MemberAccessExpressionSyntax memberAccess,
138+
string j2nTypeName,
85139
CancellationToken cancellationToken)
86140
{
87-
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
88-
if (semanticModel is null)
141+
if (memberAccess.Parent is not InvocationExpressionSyntax invocation)
89142
return document;
90143

91-
if (!TryGetJ2NTypeAndMember(semanticModel, memberAccess, out var j2nTypeName, out _))
92-
return document;
144+
var newArguments = new List<ArgumentSyntax>
145+
{
146+
SyntaxFactory.Argument(memberAccess.Expression.WithoutTrivia())
147+
};
148+
149+
if (invocation.ArgumentList is not null)
150+
newArguments.AddRange(invocation.ArgumentList.Arguments);
151+
152+
InvocationExpressionSyntax replacement = CreateJ2NToStringInvocation(j2nTypeName, newArguments)
153+
.WithTriviaFrom(invocation);
154+
155+
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
156+
editor.ReplaceNode(invocation, replacement);
157+
158+
return editor.GetChangedDocument();
159+
}
160+
161+
private async Task<Document> ReplaceConcatenationExpressionAsync(
162+
Document document,
163+
ExpressionSyntax expression,
164+
string j2nTypeName,
165+
CancellationToken cancellationToken)
166+
{
167+
var arguments = new List<ArgumentSyntax>
168+
{
169+
SyntaxFactory.Argument(expression.WithoutTrivia())
170+
};
171+
172+
InvocationExpressionSyntax replacement = CreateJ2NToStringInvocation(j2nTypeName, arguments)
173+
.WithLeadingTrivia(expression.GetLeadingTrivia())
174+
.WithTrailingTrivia(expression.GetTrailingTrivia());
175+
176+
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
177+
editor.ReplaceNode(expression, replacement);
178+
179+
return editor.GetChangedDocument();
180+
}
181+
182+
private async Task<Document> ReplaceInterpolationExpressionAsync(
183+
Document document,
184+
InterpolationSyntax interpolation,
185+
ExpressionSyntax expression,
186+
string j2nTypeName,
187+
CancellationToken cancellationToken)
188+
{
189+
var arguments = new List<ArgumentSyntax>
190+
{
191+
SyntaxFactory.Argument(expression.WithoutTrivia())
192+
};
193+
194+
var updatedInterpolation = interpolation;
195+
196+
if (interpolation.FormatClause is not null)
197+
{
198+
var formatToken = interpolation.FormatClause.FormatStringToken;
199+
var formatLiteral = SyntaxFactory.LiteralExpression(
200+
SyntaxKind.StringLiteralExpression,
201+
SyntaxFactory.Literal(formatToken.ValueText));
202+
arguments.Add(SyntaxFactory.Argument(formatLiteral));
203+
updatedInterpolation = updatedInterpolation.WithFormatClause(null);
204+
}
205+
206+
InvocationExpressionSyntax replacementExpression = CreateJ2NToStringInvocation(j2nTypeName, arguments)
207+
.WithLeadingTrivia(expression.GetLeadingTrivia())
208+
.WithTrailingTrivia(expression.GetTrailingTrivia());
209+
210+
updatedInterpolation = updatedInterpolation
211+
.WithExpression(replacementExpression)
212+
.WithAdditionalAnnotations(Formatter.Annotation);
213+
214+
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
215+
editor.ReplaceNode(interpolation, updatedInterpolation);
216+
217+
return editor.GetChangedDocument();
218+
}
93219

94-
// Build J2N.Numerics.Single/Double.ToString
95-
MemberAccessExpressionSyntax j2nToStringAccess = SyntaxFactory.MemberAccessExpression(
220+
private static InvocationExpressionSyntax CreateJ2NToStringInvocation(
221+
string j2nTypeName,
222+
IEnumerable<ArgumentSyntax> arguments)
223+
{
224+
MemberAccessExpressionSyntax j2nTypeAccess = SyntaxFactory.MemberAccessExpression(
96225
SyntaxKind.SimpleMemberAccessExpression,
97226
SyntaxFactory.MemberAccessExpression(
98227
SyntaxKind.SimpleMemberAccessExpression,
99228
SyntaxFactory.IdentifierName("J2N"),
100229
SyntaxFactory.IdentifierName("Numerics")),
101-
SyntaxFactory.IdentifierName(j2nTypeName))
102-
.WithAdditionalAnnotations(Formatter.Annotation);
230+
SyntaxFactory.IdentifierName(j2nTypeName));
103231

104-
MemberAccessExpressionSyntax fullAccess = SyntaxFactory.MemberAccessExpression(
232+
MemberAccessExpressionSyntax toStringAccess = SyntaxFactory.MemberAccessExpression(
105233
SyntaxKind.SimpleMemberAccessExpression,
106-
j2nToStringAccess,
234+
j2nTypeAccess,
107235
SyntaxFactory.IdentifierName("ToString"));
108236

109-
// Build invocation: J2N.Numerics.<Single|Double>.ToString(<expr>, <original args...>)
110-
if (memberAccess.Parent is not InvocationExpressionSyntax invocation)
111-
return document;
237+
return SyntaxFactory.InvocationExpression(
238+
toStringAccess,
239+
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments)))
240+
.WithAdditionalAnnotations(Formatter.Annotation);
241+
}
112242

113-
var newArgs = new List<ArgumentSyntax> { SyntaxFactory.Argument(memberAccess.Expression) };
114-
if (invocation.ArgumentList != null)
115-
newArgs.AddRange(invocation.ArgumentList.Arguments);
116243

117-
InvocationExpressionSyntax newInvocation = SyntaxFactory.InvocationExpression(
118-
fullAccess,
119-
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(newArgs)))
120-
.WithTriviaFrom(invocation) // safe now
121-
.WithAdditionalAnnotations(Formatter.Annotation);
244+
private static bool TryGetFloatingPointTypeName(TypeInfo typeInfo, out string typeName)
245+
{
246+
if (TryGetFloatingPointTypeName(typeInfo.Type, out typeName))
247+
return true;
122248

123-
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
124-
editor.ReplaceNode(memberAccess.Parent, newInvocation);
249+
if (TryGetFloatingPointTypeName(typeInfo.ConvertedType, out typeName))
250+
return true;
125251

126-
return editor.GetChangedDocument();
252+
typeName = null!;
253+
return false;
254+
}
255+
256+
private static bool TryGetFloatingPointTypeName(ITypeSymbol? typeSymbol, out string typeName)
257+
{
258+
typeName = typeSymbol?.SpecialType switch
259+
{
260+
SpecialType.System_Single => "Single",
261+
SpecialType.System_Double => "Double",
262+
_ => null!
263+
};
264+
265+
return typeName is not null;
127266
}
128267

129268
private static bool TryGetJ2NTypeAndMember(
@@ -137,21 +276,11 @@ private static bool TryGetJ2NTypeAndMember(
137276

138277
if (memberAccess is null)
139278
{
140-
j2nTypeName = null!; // we always return false when the value is null, so we can ignore it here.
279+
j2nTypeName = null!;
141280
return false;
142281
}
143282

144-
var typeInfo = semanticModel.GetTypeInfo(memberAccess.Expression);
145-
var type = typeInfo.Type;
146-
147-
j2nTypeName = type?.SpecialType switch
148-
{
149-
SpecialType.System_Single => "Single",
150-
SpecialType.System_Double => "Double",
151-
_ => null! // we always return false when the value is null, so we can ignore it here.
152-
};
153-
154-
if (j2nTypeName is null)
283+
if (!TryGetFloatingPointTypeName(semanticModel.GetTypeInfo(memberAccess.Expression), out j2nTypeName))
155284
return false;
156285

157286
return true;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
namespace Lucene.Net.CodeAnalysis.Dev.Sample;
21+
22+
public class LuceneDev1006Sample
23+
{
24+
private readonly float levelBottom = 1f;
25+
private readonly double maxLevel = 2d;
26+
27+
public string DescribeConcatenation()
28+
{
29+
return "" level "" + levelBottom + "" to "" + maxLevel;
30+
}
31+
32+
public string DescribeInterpolation()
33+
{
34+
return $"" level {levelBottom} to {maxLevel}"";
35+
}
36+
}

src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
Rule ID | Category | Severity | Notes
44
---------------|----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------
55
LuceneDev1005 | Design | Warning | Types in the Lucene.Net.Support namespace should not be public
6+
LuceneDev1006 | Design | Warning | Floating point values embedded in strings should be formatted with J2N.Numerics Single or Double ToString methods

0 commit comments

Comments
 (0)