Skip to content

Commit a6a5ca7

Browse files
authored
add new analyzers for StreamReader / StreamWriter (#83)
- for #81
1 parent d0d1959 commit a6a5ca7

File tree

11 files changed

+378
-0
lines changed

11 files changed

+378
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Collections.Generic;
2+
using System.IO.Abstractions.Analyzers.Analyzers.FileSystemTypeAnalyzers;
3+
using Microsoft.CodeAnalysis;
4+
using Roslyn.Testing.Analyzer;
5+
using Roslyn.Testing.Model;
6+
using Xunit;
7+
8+
namespace System.IO.Abstractions.Analyzers.Tests.Analyzers
9+
{
10+
public class StreamReaderAnalyzerTests : CSharpDiagnosticAnalyzerTest<StreamReaderAnalyzer>
11+
{
12+
[Theory]
13+
[InlineData("UsingStream.txt")]
14+
public void Analyzer_is_not_triggered(string filename)
15+
{
16+
var source = ReadFile(filename);
17+
VerifyNoDiagnosticTriggered(source);
18+
}
19+
20+
[Theory]
21+
[InlineData("UsingFilename.txt", 15, 19)]
22+
[InlineData("UsingFilenameAndOtherParameters.txt", 16, 19)]
23+
public void Analyzer_is_triggered(string filename, int diagnosticLine, int diagnosticColumn)
24+
{
25+
var source = ReadFile(filename);
26+
27+
var expectedDiagnostic = new DiagnosticResult
28+
{
29+
Id = StreamReaderAnalyzer.DiagnosticId,
30+
Message = StreamReaderAnalyzer.MessageFormat,
31+
Severity = DiagnosticSeverity.Warning,
32+
Locations = new[] { new DiagnosticResultLocation("Test0.cs", diagnosticLine, diagnosticColumn) }
33+
};
34+
35+
VerifyDiagnostic(source, expectedDiagnostic);
36+
}
37+
38+
[Fact]
39+
public void Empty_source_code_does_not_trigger_analyzer()
40+
{
41+
var source = string.Empty;
42+
VerifyNoDiagnosticTriggered(source);
43+
}
44+
45+
protected override IEnumerable<MetadataReference> GetAdditionalReferences() => new[]
46+
{
47+
MetadataReference.CreateFromFile(typeof(IFileSystem).Assembly.Location)
48+
};
49+
}
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Collections.Generic;
2+
using System.IO.Abstractions.Analyzers.Analyzers.FileSystemTypeAnalyzers;
3+
using Microsoft.CodeAnalysis;
4+
using Roslyn.Testing.Analyzer;
5+
using Roslyn.Testing.Model;
6+
using Xunit;
7+
8+
namespace System.IO.Abstractions.Analyzers.Tests.Analyzers
9+
{
10+
public class StreamWriterAnalyzerTests : CSharpDiagnosticAnalyzerTest<StreamWriterAnalyzer>
11+
{
12+
[Theory]
13+
[InlineData("UsingStream.txt")]
14+
public void Analyzer_is_not_triggered(string filename)
15+
{
16+
var source = ReadFile(filename);
17+
VerifyNoDiagnosticTriggered(source);
18+
}
19+
20+
[Theory]
21+
[InlineData("UsingFilename.txt", 15, 19)]
22+
[InlineData("UsingFilenameAndOtherParameters.txt", 16, 19)]
23+
public void Analyzer_is_triggered(string filename, int diagnosticLine, int diagnosticColumn)
24+
{
25+
var source = ReadFile(filename);
26+
27+
var expectedDiagnostic = new DiagnosticResult
28+
{
29+
Id = StreamWriterAnalyzer.DiagnosticId,
30+
Message = StreamWriterAnalyzer.MessageFormat,
31+
Severity = DiagnosticSeverity.Warning,
32+
Locations = new[] { new DiagnosticResultLocation("Test0.cs", diagnosticLine, diagnosticColumn) }
33+
};
34+
35+
VerifyDiagnostic(source, expectedDiagnostic);
36+
}
37+
38+
[Fact]
39+
public void Empty_source_code_does_not_trigger_analyzer()
40+
{
41+
var source = string.Empty;
42+
VerifyNoDiagnosticTriggered(source);
43+
}
44+
45+
protected override IEnumerable<MetadataReference> GetAdditionalReferences() => new[]
46+
{
47+
MetadataReference.CreateFromFile(typeof(IFileSystem).Assembly.Location)
48+
};
49+
}
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.IO;
2+
3+
namespace SomeNameSpace
4+
{
5+
public class UsingFilename
6+
{
7+
public UsingFilename()
8+
{
9+
}
10+
11+
public void SomeMethod()
12+
{
13+
const string path = "C:\\temp.txt";
14+
15+
using (var x = new StreamReader(path))
16+
{
17+
;
18+
}
19+
}
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.IO;
2+
using System.Text;
3+
4+
namespace SomeNameSpace
5+
{
6+
public class UsingFilenameAndOtherParameters
7+
{
8+
public UsingFilenameAndOtherParameters()
9+
{
10+
}
11+
12+
public void SomeMethod()
13+
{
14+
const string path = "C:\\temp.txt";
15+
16+
using (var x = new StreamReader(path, Encoding.UTF8))
17+
{
18+
;
19+
}
20+
}
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.IO;
2+
using System.IO.Abstractions;
3+
using System.Text;
4+
5+
namespace SomeNameSpace
6+
{
7+
public class UsingStream
8+
{
9+
private readonly IFileSystem _fileSystem;
10+
11+
public UsingStream(IFileSystem fileSystem)
12+
{
13+
_fileSystem = fileSystem;
14+
}
15+
16+
public void SomeMethod()
17+
{
18+
const string path = "C:\\temp.txt";
19+
var stream = _fileSystem.FileStream.Create(path, FileMode.Open);
20+
using (var writer = new StreamReader(stream))
21+
{
22+
;
23+
}
24+
25+
using (var writer = new StreamReader(stream, Encoding.UTF8))
26+
{
27+
;
28+
}
29+
}
30+
}
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.IO;
2+
3+
namespace SomeNameSpace
4+
{
5+
public class UsingFilename
6+
{
7+
public UsingFilename()
8+
{
9+
}
10+
11+
public void SomeMethod()
12+
{
13+
const string path = "C:\\temp.txt";
14+
15+
using (var x = new StreamWriter(path))
16+
{
17+
;
18+
}
19+
}
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.IO;
2+
using System.Text;
3+
4+
namespace SomeNameSpace
5+
{
6+
public class UsingFilenameAndOtherParameters
7+
{
8+
public UsingFilenameAndOtherParameters()
9+
{
10+
}
11+
12+
public void SomeMethod()
13+
{
14+
const string path = "C:\\temp.txt";
15+
16+
using (var x = new StreamWriter(path, true, Encoding.UTF8))
17+
{
18+
;
19+
}
20+
}
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.IO;
2+
using System.IO.Abstractions;
3+
using System.Text;
4+
5+
namespace SomeNameSpace
6+
{
7+
public class UsingStream
8+
{
9+
private readonly IFileSystem _fileSystem;
10+
11+
public UsingStream(IFileSystem fileSystem)
12+
{
13+
_fileSystem = fileSystem;
14+
}
15+
16+
public void SomeMethod()
17+
{
18+
const string path = "C:\\temp.txt";
19+
var stream = _fileSystem.FileStream.Create(path, FileMode.Open);
20+
using (var writer = new StreamWriter(stream))
21+
{
22+
;
23+
}
24+
25+
using (var writer = new StreamWriter(stream, true, Encoding.UTF8))
26+
{
27+
;
28+
}
29+
}
30+
}
31+
}

System.IO.Abstractions.Analyzers/Analyzers/BaseFileSystemNodeAnalyzer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,13 @@ INamespaceSymbol namespaceSymbol
9595
&& !namespaceSymbol.IsGlobalNamespace
9696
&& namespaceSymbol.ToString().StartsWith(Constants.FileSystemNameSpace, StringComparison.Ordinal);
9797
}
98+
99+
protected static bool IsFirstConstructorParameterOfType<T>(SyntaxNodeAnalysisContext context, ExpressionSyntax syntax)
100+
{
101+
return (syntax as ObjectCreationExpressionSyntax)?.ArgumentList?.Arguments.FirstOrDefault() is ArgumentSyntax firstArgument
102+
&& (context.SemanticModel.GetSymbolInfo(firstArgument.Expression).Symbol as ILocalSymbol)?.Type is ITypeSymbol argumentType
103+
&& argumentType.ContainingNamespace.Name == typeof(T).Namespace
104+
&& argumentType.Name == typeof(T).Name;
105+
}
98106
}
99107
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Collections.Immutable;
2+
using JetBrains.Annotations;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace System.IO.Abstractions.Analyzers.Analyzers.FileSystemTypeAnalyzers
8+
{
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class StreamReaderAnalyzer : BaseFileSystemNodeAnalyzer
11+
{
12+
/// <summary>
13+
/// Diagnostic Identifier
14+
/// </summary>
15+
[UsedImplicitly]
16+
public const string DiagnosticId = "IO0011";
17+
18+
/// <summary>
19+
/// Diagnostic Title
20+
/// </summary>
21+
private const string Title = "Replace StreamReader string constructor overload with stream based overload"
22+
+ " using a stream from IFileSystem.FileStream for improved testability";
23+
24+
/// <summary>
25+
/// Diagnostic Message Format
26+
/// </summary>
27+
public const string MessageFormat = Title;
28+
29+
/// <summary>
30+
/// Diagnostic Description
31+
/// </summary>
32+
private const string Description = Title;
33+
34+
/// <summary>
35+
/// Правило
36+
/// </summary>
37+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId,
38+
Title,
39+
MessageFormat,
40+
Category,
41+
DiagnosticSeverity.Warning,
42+
true,
43+
Description);
44+
45+
/// <inheritdoc />
46+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
47+
48+
/// <inheritdoc />
49+
protected override void Analyze(SyntaxNodeAnalysisContext context, ExpressionSyntax syntax)
50+
{
51+
if (IsFirstConstructorParameterOfType<string>(context, syntax))
52+
context.ReportDiagnostic(Diagnostic.Create(Rule, syntax.GetLocation()));
53+
}
54+
55+
/// <inheritdoc />
56+
protected override Type GetFileSystemType()
57+
{
58+
return typeof(StreamReader);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)