Skip to content

Commit 2039395

Browse files
committed
Merge branch 'f/RefactoringInjectionDiagnostic' into develop
2 parents c2bfd27 + 2187c0c commit 2039395

7 files changed

Lines changed: 139 additions & 50 deletions

File tree

System.IO.Abstractions.Analyzers.Tests/Analyzers/FileServiceInterfaceInjectionAnalyzerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void Analyzer_is_not_triggered(string filename)
1818
}
1919

2020
[Theory]
21-
[InlineData("WithOutFileSystem.txt", 7, 10)]
21+
[InlineData("WithOutFileSystem.txt", 5, 15)]
2222
public void Analyzer_is_triggered(string filename, int diagnosticLine, int diagnosticColumn)
2323
{
2424
var source = ReadFile(filename);

System.IO.Abstractions.Analyzers.Tests/CodeFixes/FileServiceInterfaceInjectionCodeFixTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class FileServiceInterfaceInjectionCodeFixTests :
1212
{
1313
[Theory]
1414
[InlineData("BeforeFix.txt", "AfterFix.txt")]
15+
[InlineData("BeforeFixWithoutConstructor.txt", "AfterFix.txt")]
1516
public void CodeFix(string sourceBefore, string sourceAfter)
1617
{
1718
var sourceBeforeFix = ReadFile(sourceBefore);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.IO;
2+
3+
namespace SomeNameSpace
4+
{
5+
public class WithOutFileSystem
6+
{
7+
public void SomeMethod()
8+
{
9+
const string filePath = "C:\\temp.txt";
10+
11+
if (File.Exists(filePath))
12+
{
13+
File.Delete(filePath);
14+
}
15+
}
16+
}
17+
}

System.IO.Abstractions.Analyzers/Analyzers/FileServiceInterfaceInjectionAnalyzer.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,25 @@ protected override void AnalyzeCompilation(CompilationStartAnalysisContext compi
5252
{
5353
compilationStartContext.RegisterSyntaxNodeAction(syntaxContext =>
5454
{
55-
var constructor = (ConstructorDeclarationSyntax) syntaxContext.Node;
55+
var classDeclarationSyntax = (ClassDeclarationSyntax) syntaxContext.Node;
5656

57-
var compilationUnitSyntax = GetCompilationUnit(constructor);
57+
var compilationUnitSyntax = GetCompilationUnit(classDeclarationSyntax);
5858

5959
if (compilationUnitSyntax.Usings.All(x => x.Name.NormalizeWhitespace().ToFullString() != typeof(Path).Namespace))
6060
{
6161
return;
6262
}
6363

64-
var fileSystem = constructor.ParameterList.Parameters.FirstOrDefault(x =>
65-
x.Type.NormalizeWhitespace().ToFullString() == fileSystemContext.FileSystemType.Name);
64+
var fileSystem = classDeclarationSyntax.Members.OfType<FieldDeclarationSyntax>().FirstOrDefault(x =>
65+
x.NormalizeWhitespace().ToFullString() == fileSystemContext.FileSystemType.Name);
6666

6767
if (fileSystem == null)
6868
{
6969
syntaxContext.ReportDiagnostic(Diagnostic.Create(Rule,
70-
constructor.Identifier.GetLocation()));
70+
classDeclarationSyntax.Identifier.GetLocation()));
7171
}
7272
},
73-
SyntaxKind.ConstructorDeclaration);
73+
SyntaxKind.ClassDeclaration);
7474
}
7575

7676
private CompilationUnitSyntax GetCompilationUnit(SyntaxNode node)

System.IO.Abstractions.Analyzers/CodeActions/FileServiceInterfaceInjectionCodeAction.cs

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ namespace System.IO.Abstractions.Analyzers.CodeActions
1515
/// <inheritdoc />
1616
public class FileServiceInterfaceInjectionCodeAction : CodeAction
1717
{
18-
private readonly ConstructorDeclarationSyntax _constructor;
18+
private const string FieldFileSystemName = "_fileSystem";
19+
20+
private const string ParameterFileSystemName = "fileSystem";
21+
22+
private readonly ClassDeclarationSyntax _class;
1923

2024
private readonly Document _document;
2125

22-
public FileServiceInterfaceInjectionCodeAction(string title, Document document, ConstructorDeclarationSyntax constructor)
26+
public FileServiceInterfaceInjectionCodeAction(string title, Document document, ClassDeclarationSyntax @class)
2327
{
24-
_constructor = constructor;
28+
_class = @class;
2529
_document = document;
2630
Title = title;
2731
}
@@ -34,76 +38,70 @@ protected override async Task<Document> GetChangedDocumentAsync(CancellationToke
3438
{
3539
var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
3640

37-
var parameter = CreateFileSystemParameterDeclaration();
38-
39-
if (!(_constructor.Parent is ClassDeclarationSyntax classDeclarationSyntax))
41+
if (!HasFileSystemProperty(_class))
4042
{
41-
editor.AddParameter(_constructor, parameter);
42-
43-
return editor.GetChangedDocument();
43+
editor.InsertMembers(_class,
44+
0,
45+
new SyntaxNode[]
46+
{
47+
CreateFileSystemPropertyDeclaration()
48+
});
4449
}
4550

46-
var fileSystem = classDeclarationSyntax.Members
47-
.OfType<FieldDeclarationSyntax>()
48-
.FirstOrDefault(x => x.NormalizeWhitespace().ToFullString().Equals(Constants.FileSystemName));
51+
ConstructorAddParameter(_class, editor);
4952

50-
if (fileSystem != null)
53+
var compilationUnitSyntax = GetCompilationUnit(_class);
54+
var fileSystemUsing = GetFileSystemUsing();
55+
56+
if (compilationUnitSyntax.Usings.Any())
5157
{
52-
return editor.GetChangedDocument();
58+
editor.ReplaceNode(GetSystemIoUsing(compilationUnitSyntax),
59+
fileSystemUsing);
5360
}
5461

55-
var fileSystemPropertyDeclaration = CreateFileSystemPropertyDeclaration();
56-
57-
editor.InsertMembers(classDeclarationSyntax,
58-
0,
59-
new SyntaxNode[]
60-
{
61-
fileSystemPropertyDeclaration
62-
});
63-
64-
var newConstructor = _constructor.WithBody(_constructor.Body.AddStatements(CreateAssignmentExpression()))
65-
.AddParameterListParameters(parameter)
66-
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation)
67-
.NormalizeWhitespace();
68-
69-
editor.ReplaceNode(_constructor, newConstructor);
70-
var compilationUnitSyntax = GetCompilationUnit(_constructor);
62+
return editor.GetChangedDocument();
63+
}
7164

72-
editor.ReplaceNode(compilationUnitSyntax.Usings.FirstOrDefault(),
73-
SF.UsingDirective(SF.ParseName(Constants.FileSystemNameSpace)));
65+
private static UsingDirectiveSyntax GetFileSystemUsing()
66+
{
67+
return SF.UsingDirective(SF.ParseName(Constants.FileSystemNameSpace));
68+
}
7469

75-
return editor.GetChangedDocument();
70+
private static UsingDirectiveSyntax GetSystemIoUsing(CompilationUnitSyntax unit)
71+
{
72+
return unit.Usings.FirstOrDefault(x =>
73+
x.Name.NormalizeWhitespace().ToFullString().Equals(typeof(Path).Namespace));
7674
}
7775

7876
private static FieldDeclarationSyntax CreateFileSystemPropertyDeclaration()
7977
{
80-
return SF.FieldDeclaration(SF.VariableDeclaration(CreateFileSystemType())
81-
.WithVariables(SF.SingletonSeparatedList(SF.VariableDeclarator(SF.Identifier("_fileSystem")))))
78+
return SF.FieldDeclaration(SF.VariableDeclaration(GetFileSystemType())
79+
.WithVariables(SF.SingletonSeparatedList(SF.VariableDeclarator(SF.Identifier(FieldFileSystemName)))))
8280
.WithModifiers(SF.TokenList(SF.Token(SyntaxKind.PrivateKeyword),
8381
SF.Token(SyntaxKind.ReadOnlyKeyword)));
8482
}
8583

8684
private static ParameterSyntax CreateFileSystemParameterDeclaration()
8785
{
88-
return SF.Parameter(SF.Identifier("fileSystem"))
89-
.WithType(CreateFileSystemType())
86+
return SF.Parameter(SF.Identifier(ParameterFileSystemName))
87+
.WithType(GetFileSystemType())
9088
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.SpecialTypeAnnotation)
9189
.NormalizeWhitespace();
9290
}
9391

94-
private static TypeSyntax CreateFileSystemType()
92+
private static TypeSyntax GetFileSystemType()
9593
{
9694
return SF.ParseTypeName(Constants.FileSystemName);
9795
}
9896

9997
private static ExpressionStatementSyntax CreateAssignmentExpression()
10098
{
10199
return SF.ExpressionStatement(SF.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
102-
SF.IdentifierName("_fileSystem"),
103-
SF.IdentifierName("fileSystem")));
100+
SF.IdentifierName(FieldFileSystemName),
101+
SF.IdentifierName(ParameterFileSystemName)));
104102
}
105103

106-
private CompilationUnitSyntax GetCompilationUnit(SyntaxNode node)
104+
private static CompilationUnitSyntax GetCompilationUnit(SyntaxNode node)
107105
{
108106
switch (node)
109107
{
@@ -118,5 +116,75 @@ private CompilationUnitSyntax GetCompilationUnit(SyntaxNode node)
118116
return GetCompilationUnit(node.Parent);
119117
}
120118
}
119+
120+
private static bool HasFileSystemProperty(TypeDeclarationSyntax classDeclaration)
121+
{
122+
return classDeclaration.Members.OfType<PropertyDeclarationSyntax>()
123+
.Any(x => x.Identifier.Text == FieldFileSystemName && x.Type == GetFileSystemType());
124+
}
125+
126+
private static ConstructorDeclarationSyntax GetConstructor(SyntaxNode classDeclaration)
127+
{
128+
return classDeclaration.ChildNodes().OfType<ConstructorDeclarationSyntax>().FirstOrDefault();
129+
}
130+
131+
private static bool ConstructorHasFileSystemParameter(BaseMethodDeclarationSyntax constructor)
132+
{
133+
return constructor.ParameterList.Parameters
134+
.Any(x => x.Identifier.Text == ParameterFileSystemName && x.Type == GetFileSystemType());
135+
}
136+
137+
private static bool ConstructorHasAssignmentExpression(BaseMethodDeclarationSyntax constructor)
138+
{
139+
if (constructor.Body == null)
140+
{
141+
return false;
142+
}
143+
144+
return constructor.Body.Statements.OfType<ExpressionStatementSyntax>()
145+
.Any(x => x.IsKind(SyntaxKind.SimpleAssignmentExpression)
146+
&& x.Expression.Contains(SF.IdentifierName(FieldFileSystemName))
147+
&& x.Expression.Contains(SF.IdentifierName(ParameterFileSystemName)));
148+
}
149+
150+
private static bool HasConstructor(SyntaxNode classDeclaration)
151+
{
152+
return classDeclaration.ChildNodes().OfType<ConstructorDeclarationSyntax>().Any();
153+
}
154+
155+
private static void ConstructorAddParameter(ClassDeclarationSyntax classDeclaration, SyntaxEditor editor)
156+
{
157+
var constructor = HasConstructor(classDeclaration)
158+
? GetConstructor(classDeclaration)
159+
: SF.ConstructorDeclaration(classDeclaration.Identifier)
160+
.WithModifiers(SyntaxTokenList.Create(SyntaxFactory.Token(SyntaxKind.PublicKeyword)));
161+
162+
var newConstructor = constructor.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation)
163+
.NormalizeWhitespace();
164+
165+
if (!ConstructorHasAssignmentExpression(newConstructor))
166+
{
167+
newConstructor = newConstructor.AddBodyStatements(CreateAssignmentExpression());
168+
}
169+
170+
if (!ConstructorHasFileSystemParameter(newConstructor))
171+
{
172+
var parameter = CreateFileSystemParameterDeclaration();
173+
newConstructor = newConstructor.AddParameterListParameters(parameter);
174+
}
175+
176+
if (HasConstructor(classDeclaration))
177+
{
178+
editor.ReplaceNode(constructor, newConstructor);
179+
} else
180+
{
181+
editor.InsertBefore(GetMethod(classDeclaration), newConstructor);
182+
}
183+
}
184+
185+
private static MethodDeclarationSyntax GetMethod(ClassDeclarationSyntax classDeclaration)
186+
{
187+
return classDeclaration.ChildNodes().OfType<MethodDeclarationSyntax>().FirstOrDefault();
188+
}
121189
}
122190
}

System.IO.Abstractions.Analyzers/CodeFixes/FileServiceInterfaceInjectionCodeFix.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ public class FileServiceInterfaceInjectionCodeFix : CodeFixProvider
2323
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2424
{
2525
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
26-
var constructorDeclarationSyntax = root.FindNode(context.Span).FirstAncestorOrSelf<ConstructorDeclarationSyntax>();
26+
var classDeclarationSyntax = root.FindNode(context.Span).FirstAncestorOrSelf<ClassDeclarationSyntax>();
2727

2828
context.RegisterCodeFix(new FileServiceInterfaceInjectionCodeAction(Title,
2929
context.Document,
30-
constructorDeclarationSyntax),
30+
classDeclarationSyntax),
3131
context.Diagnostics);
3232
}
3333
}

System.IO.Abstractions.Analyzers/System.IO.Abstractions.Analyzers.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,7 @@
4747
<None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="" />
4848
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
4949
</ItemGroup>
50+
<ItemGroup>
51+
<Folder Include="Helpers" />
52+
</ItemGroup>
5053
</Project>

0 commit comments

Comments
 (0)