Skip to content

Commit ba3ba01

Browse files
Add analyzer and code fix for floating point string formatting (#9)
* 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. * LuceneDev1006Sample.cs: Fixed issue with doubling of quotes that caused the sample not to compile. * Preserve alignment clause in interpolation code fix Ensures that the alignment clause is retained when replacing interpolation expressions in LuceneDev1001_FloatingPointFormattingCSCodeFixProvider. Also fixes string formatting in LuceneDev1006Sample to use correct quotes and spacing. * Preserve interpolation alignment and add coverage --------- Co-authored-by: Shad Storhaug <shad@shadstorhaug.com>
1 parent dea6328 commit ba3ba01

File tree

11 files changed

+1045
-53
lines changed

11 files changed

+1045
-53
lines changed

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

Lines changed: 188 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,239 @@ 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());
93175

94-
// Build J2N.Numerics.Single/Double.ToString
95-
MemberAccessExpressionSyntax j2nToStringAccess = SyntaxFactory.MemberAccessExpression(
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+
var alignmentClause = interpolation.AlignmentClause;
196+
197+
if (interpolation.FormatClause is not null)
198+
{
199+
var formatToken = interpolation.FormatClause.FormatStringToken;
200+
var formatLiteral = SyntaxFactory.LiteralExpression(
201+
SyntaxKind.StringLiteralExpression,
202+
SyntaxFactory.Literal(formatToken.ValueText));
203+
arguments.Add(SyntaxFactory.Argument(formatLiteral));
204+
updatedInterpolation = updatedInterpolation.WithFormatClause(null);
205+
}
206+
207+
InvocationExpressionSyntax replacementExpression = CreateJ2NToStringInvocation(j2nTypeName, arguments)
208+
.WithLeadingTrivia(expression.GetLeadingTrivia())
209+
.WithTrailingTrivia(expression.GetTrailingTrivia());
210+
211+
updatedInterpolation = updatedInterpolation.WithExpression(replacementExpression);
212+
213+
if (alignmentClause is not null)
214+
{
215+
updatedInterpolation = updatedInterpolation.WithAlignmentClause(alignmentClause);
216+
}
217+
218+
updatedInterpolation = updatedInterpolation.WithAdditionalAnnotations(Formatter.Annotation);
219+
220+
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
221+
editor.ReplaceNode(interpolation, updatedInterpolation);
222+
223+
return editor.GetChangedDocument();
224+
}
225+
226+
private static InvocationExpressionSyntax CreateJ2NToStringInvocation(
227+
string j2nTypeName,
228+
IEnumerable<ArgumentSyntax> arguments)
229+
{
230+
MemberAccessExpressionSyntax j2nTypeAccess = SyntaxFactory.MemberAccessExpression(
96231
SyntaxKind.SimpleMemberAccessExpression,
97232
SyntaxFactory.MemberAccessExpression(
98233
SyntaxKind.SimpleMemberAccessExpression,
99234
SyntaxFactory.IdentifierName("J2N"),
100235
SyntaxFactory.IdentifierName("Numerics")),
101-
SyntaxFactory.IdentifierName(j2nTypeName))
102-
.WithAdditionalAnnotations(Formatter.Annotation);
236+
SyntaxFactory.IdentifierName(j2nTypeName));
103237

104-
MemberAccessExpressionSyntax fullAccess = SyntaxFactory.MemberAccessExpression(
238+
MemberAccessExpressionSyntax toStringAccess = SyntaxFactory.MemberAccessExpression(
105239
SyntaxKind.SimpleMemberAccessExpression,
106-
j2nToStringAccess,
240+
j2nTypeAccess,
107241
SyntaxFactory.IdentifierName("ToString"));
108242

109-
// Build invocation: J2N.Numerics.<Single|Double>.ToString(<expr>, <original args...>)
110-
if (memberAccess.Parent is not InvocationExpressionSyntax invocation)
111-
return document;
243+
return SyntaxFactory.InvocationExpression(
244+
toStringAccess,
245+
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments)))
246+
.WithAdditionalAnnotations(Formatter.Annotation);
247+
}
112248

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

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

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

126-
return editor.GetChangedDocument();
258+
typeName = null!;
259+
return false;
260+
}
261+
262+
private static bool TryGetFloatingPointTypeName(ITypeSymbol? typeSymbol, out string typeName)
263+
{
264+
typeName = typeSymbol?.SpecialType switch
265+
{
266+
SpecialType.System_Single => "Single",
267+
SpecialType.System_Double => "Double",
268+
_ => null!
269+
};
270+
271+
return typeName is not null;
127272
}
128273

129274
private static bool TryGetJ2NTypeAndMember(
@@ -137,21 +282,11 @@ private static bool TryGetJ2NTypeAndMember(
137282

138283
if (memberAccess is null)
139284
{
140-
j2nTypeName = null!; // we always return false when the value is null, so we can ignore it here.
285+
j2nTypeName = null!;
141286
return false;
142287
}
143288

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)
289+
if (!TryGetFloatingPointTypeName(semanticModel.GetTypeInfo(memberAccess.Expression), out j2nTypeName))
155290
return false;
156291

157292
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)