Skip to content

Commit 34bab89

Browse files
feat: Add code fix for INTL0301/INTL0302 (FavorDirectoryEnumerationCalls)
- Add public DiagnosticId301/302 constants to the analyzer (required by the code fix) - Add FavorDirectoryEnumerationCalls CodeFixProvider that renames GetFiles/GetDirectories to their lazy Enumerate* counterparts, wrapping with .ToArray() when the result is assigned to / returned as string[] - Add System.Runtime metadata reference to test helper to support LINQ-based code fixes - Add 4 code fix tests covering both rename and .ToArray() wrapping scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e888619 commit 34bab89

File tree

4 files changed

+429
-5
lines changed

4 files changed

+429
-5
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeActions;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using Microsoft.CodeAnalysis.Formatting;
12+
using Microsoft.CodeAnalysis.Text;
13+
14+
namespace IntelliTect.Analyzer.CodeFixes
15+
{
16+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FavorDirectoryEnumerationCalls))]
17+
[Shared]
18+
public class FavorDirectoryEnumerationCalls : CodeFixProvider
19+
{
20+
private const string TitleGetFiles = "Use Directory.EnumerateFiles";
21+
private const string TitleGetDirectories = "Use Directory.EnumerateDirectories";
22+
23+
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
24+
ImmutableArray.Create(
25+
Analyzers.FavorDirectoryEnumerationCalls.DiagnosticId301,
26+
Analyzers.FavorDirectoryEnumerationCalls.DiagnosticId302);
27+
28+
public sealed override FixAllProvider GetFixAllProvider() =>
29+
WellKnownFixAllProviders.BatchFixer;
30+
31+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
{
33+
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
34+
35+
Diagnostic diagnostic = context.Diagnostics.First();
36+
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
37+
38+
// The diagnostic span covers the full invocation expression (Directory.GetFiles(...))
39+
InvocationExpressionSyntax invocation = root.FindToken(diagnosticSpan.Start)
40+
.Parent.AncestorsAndSelf()
41+
.OfType<InvocationExpressionSyntax>()
42+
.First();
43+
44+
bool isGetFiles = diagnostic.Id == Analyzers.FavorDirectoryEnumerationCalls.DiagnosticId301;
45+
string title = isGetFiles ? TitleGetFiles : TitleGetDirectories;
46+
string newMethodName = isGetFiles ? "EnumerateFiles" : "EnumerateDirectories";
47+
48+
context.RegisterCodeFix(
49+
CodeAction.Create(
50+
title: title,
51+
createChangedDocument: c => UseEnumerationMethodAsync(context.Document, invocation, newMethodName, c),
52+
equivalenceKey: title),
53+
diagnostic);
54+
}
55+
56+
private static async Task<Document> UseEnumerationMethodAsync(
57+
Document document,
58+
InvocationExpressionSyntax invocation,
59+
string newMethodName,
60+
CancellationToken cancellationToken)
61+
{
62+
var memberAccess = (MemberAccessExpressionSyntax)invocation.Expression;
63+
64+
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
65+
66+
// Rename: Directory.GetFiles(...) → Directory.EnumerateFiles(...)
67+
InvocationExpressionSyntax renamedInvocation = invocation.WithExpression(
68+
memberAccess.WithName(SyntaxFactory.IdentifierName(newMethodName)));
69+
70+
ExpressionSyntax replacement = NeedsToArrayWrapper(invocation, semanticModel, cancellationToken)
71+
// Wrap as Directory.EnumerateFiles(...).ToArray()
72+
? SyntaxFactory.InvocationExpression(
73+
SyntaxFactory.MemberAccessExpression(
74+
SyntaxKind.SimpleMemberAccessExpression,
75+
renamedInvocation,
76+
SyntaxFactory.IdentifierName("ToArray")))
77+
: renamedInvocation;
78+
79+
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
80+
SyntaxNode newRoot = oldRoot.ReplaceNode(invocation, replacement.WithAdditionalAnnotations(Formatter.Annotation));
81+
82+
if (replacement != renamedInvocation && newRoot is CompilationUnitSyntax compilationUnit)
83+
{
84+
newRoot = AddUsingIfMissing(compilationUnit, "System.Linq");
85+
}
86+
87+
return document.WithSyntaxRoot(newRoot);
88+
}
89+
90+
private static bool NeedsToArrayWrapper(
91+
InvocationExpressionSyntax invocation,
92+
SemanticModel semanticModel,
93+
CancellationToken ct)
94+
{
95+
SyntaxNode parent = invocation.Parent;
96+
97+
// string[] files = Directory.GetFiles(...) or field/property initializer
98+
if (parent is EqualsValueClauseSyntax equalsValue)
99+
{
100+
// Local variable or field: string[] files = ... / private string[] _files = ...
101+
if (equalsValue.Parent is VariableDeclaratorSyntax
102+
&& equalsValue.Parent.Parent is VariableDeclarationSyntax declaration
103+
&& semanticModel.GetTypeInfo(declaration.Type, ct).Type is IArrayTypeSymbol)
104+
{
105+
return true;
106+
}
107+
108+
// Property initializer: public string[] Files { get; } = Directory.GetFiles(...)
109+
if (equalsValue.Parent is PropertyDeclarationSyntax property
110+
&& semanticModel.GetTypeInfo(property.Type, ct).Type is IArrayTypeSymbol)
111+
{
112+
return true;
113+
}
114+
}
115+
116+
// files = Directory.GetFiles(...)
117+
if (parent is AssignmentExpressionSyntax assignment
118+
&& semanticModel.GetTypeInfo(assignment.Left, ct).Type is IArrayTypeSymbol)
119+
{
120+
return true;
121+
}
122+
123+
// return Directory.GetFiles(...) in a method or local function returning string[]
124+
if (parent is ReturnStatementSyntax)
125+
{
126+
TypeSyntax returnType = invocation.Ancestors()
127+
.Select(a => a switch
128+
{
129+
MethodDeclarationSyntax m => m.ReturnType,
130+
LocalFunctionStatementSyntax lf => lf.ReturnType,
131+
_ => null
132+
})
133+
.FirstOrDefault(t => t != null);
134+
if (returnType != null
135+
&& semanticModel.GetTypeInfo(returnType, ct).Type is IArrayTypeSymbol)
136+
{
137+
return true;
138+
}
139+
}
140+
141+
// Expression-bodied members: string[] GetFiles() => Directory.GetFiles(...)
142+
if (parent is ArrowExpressionClauseSyntax arrow)
143+
{
144+
TypeSyntax returnType = arrow.Parent switch
145+
{
146+
MethodDeclarationSyntax m => m.ReturnType,
147+
LocalFunctionStatementSyntax lf => lf.ReturnType,
148+
PropertyDeclarationSyntax p => p.Type,
149+
_ => null
150+
};
151+
if (returnType != null && semanticModel.GetTypeInfo(returnType, ct).Type is IArrayTypeSymbol)
152+
{
153+
return true;
154+
}
155+
}
156+
157+
// SomeMethod(Directory.GetFiles(...)) where the parameter type is string[]
158+
if (parent is ArgumentSyntax argument
159+
&& argument.Parent is ArgumentListSyntax argumentList
160+
&& argumentList.Parent is InvocationExpressionSyntax outerInvocation
161+
&& semanticModel.GetSymbolInfo(outerInvocation, ct).Symbol is IMethodSymbol outerMethod)
162+
{
163+
IParameterSymbol targetParam;
164+
165+
// Named argument: SomeMethod(param: Directory.GetFiles(...))
166+
if (argument.NameColon != null)
167+
{
168+
string paramName = argument.NameColon.Name.Identifier.Text;
169+
targetParam = outerMethod.Parameters.FirstOrDefault(p => p.Name == paramName);
170+
}
171+
else
172+
{
173+
int argIndex = argumentList.Arguments.IndexOf(argument);
174+
int paramCount = outerMethod.Parameters.Length;
175+
targetParam = argIndex >= 0 && argIndex < paramCount
176+
? outerMethod.Parameters[argIndex]
177+
: argIndex >= 0 && paramCount > 0 && outerMethod.Parameters[paramCount - 1].IsParams
178+
? outerMethod.Parameters[paramCount - 1]
179+
: null;
180+
}
181+
182+
if (targetParam?.Type is IArrayTypeSymbol)
183+
{
184+
return true;
185+
}
186+
}
187+
188+
return false;
189+
}
190+
191+
private static SyntaxNode AddUsingIfMissing(CompilationUnitSyntax root, string namespaceName)
192+
{
193+
bool alreadyPresent = root.Usings.Any(u => u.Name?.ToString() == namespaceName);
194+
if (alreadyPresent)
195+
{
196+
return root;
197+
}
198+
199+
UsingDirectiveSyntax newUsing = SyntaxFactory.UsingDirective(
200+
SyntaxFactory.ParseName(namespaceName))
201+
.NormalizeWhitespace()
202+
.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);
203+
204+
return root.AddUsings(newUsing);
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)