diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExclusionTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExclusionTests.cs new file mode 100644 index 000000000..62502a792 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExclusionTests.cs @@ -0,0 +1,86 @@ +namespace StyleCop.Analyzers.Test +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.Diagnostics; + using TestHelper; + using Xunit; + + /// + /// Unit tests for testing exclusion of auto generated files. + /// + public class ExclusionTests : CodeFixVerifier + { + /// + /// Gets the statements that will be used in the theory test cases. + /// + /// + /// The statements that will be used in the theory test cases. + /// + public static IEnumerable ShouldBeExcluded + { + get + { + yield return new[] { "Test.cs", string.Empty }; + yield return new[] { "Test.cs", " " }; + yield return new[] { "Test.cs", "\r\n\r\n" }; + yield return new[] { "Test.cs", "\t" }; + yield return new[] { "Test.designer.cs", "class Foo { }" }; + yield return new[] { "Test.cs", "// + /// Gets the statements that will be used in the theory test cases. + /// + /// + /// The statements that will be used in the theory test cases. + /// + public static IEnumerable ShouldNotBeExcluded + { + get + { + yield return new[] { "Test.designerr.cs", "class Foo { }" }; + yield return new[] { "Test.cs", "// + /// Verifies that the source file is excluded from analysis. + /// + /// The filename + /// The code to test + /// A representing the asynchronous unit test. + [Theory] + [MemberData(nameof(ShouldBeExcluded))] + public async Task TestIsExcludedAsync(string filename, string testCode) + { + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None, filename: filename).ConfigureAwait(false); + } + + /// + /// Verifies that the source file is not excluded from analysis. + /// + /// The filename + /// The code to test + /// A representing the asynchronous unit test. + [Theory] + [MemberData(nameof(ShouldNotBeExcluded))] + public async Task TestIsNotExcludedAsync(string filename, string testCode) + { + var result = this.CSharpDiagnostic().WithLocation(filename, 1, 1); + + await this.VerifyCSharpDiagnosticAsync(testCode, result, CancellationToken.None, filename: filename).ConfigureAwait(false); + } + + /// + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new ExclusionTestAnalyzer(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticResult.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticResult.cs index 4c45ec619..b058d0fad 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticResult.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticResult.cs @@ -161,5 +161,17 @@ public DiagnosticResult WithLocation(string path, int line, int column) result.locations[result.locations.Length - 1] = new DiagnosticResultLocation(path, line, column); return result; } + + public DiagnosticResult WithLineOffset(int offset) + { + DiagnosticResult result = this; + Array.Resize(ref result.locations, result.locations?.Length ?? 0); + for (int i = 0; i < result.locations.Length; i++) + { + result.locations[i].Line += offset; + } + + return result; + } } } \ No newline at end of file diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs index dea4c45de..2fea3d7d7 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs @@ -40,11 +40,12 @@ public abstract partial class DiagnosticVerifier /// class. /// The analyzers to be run on the sources. /// The that the task will observe. + /// The filenames or null if the default filename should be used /// A collection of s that surfaced in the source code, sorted by /// . - private Task> GetSortedDiagnosticsAsync(string[] sources, string language, ImmutableArray analyzers, CancellationToken cancellationToken) + private Task> GetSortedDiagnosticsAsync(string[] sources, string language, ImmutableArray analyzers, CancellationToken cancellationToken, string[] filenames) { - return GetSortedDiagnosticsFromDocumentsAsync(analyzers, this.GetDocuments(sources, language), cancellationToken); + return GetSortedDiagnosticsFromDocumentsAsync(analyzers, this.GetDocuments(sources, language, filenames), cancellationToken); } /// @@ -135,20 +136,16 @@ private static Diagnostic[] SortDistinctDiagnostics(IEnumerable diag /// Classes in the form of strings. /// The language the source classes are in. Values may be taken from the /// class. + /// The filenames or null if the default filename should be used /// A collection of s representing the sources. - private Document[] GetDocuments(string[] sources, string language) + private Document[] GetDocuments(string[] sources, string language, string[] filenames) { if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) { throw new ArgumentException("Unsupported Language"); } - for (int i = 0; i < sources.Length; i++) - { - string fileName = language == LanguageNames.CSharp ? "Test" + i + ".cs" : "Test" + i + ".vb"; - } - - var project = this.CreateProject(sources, language); + var project = this.CreateProject(sources, language, filenames); var documents = project.Documents.ToArray(); if (sources.Length != documents.Length) @@ -177,9 +174,10 @@ protected Document CreateDocument(string source, string language = LanguageNames /// Classes in the form of strings. /// The language the source classes are in. Values may be taken from the /// class. + /// The filenames or null if the default filename should be used /// A created out of the s created from the source /// strings. - private Project CreateProject(string[] sources, string language = LanguageNames.CSharp) + private Project CreateProject(string[] sources, string language = LanguageNames.CSharp, string[] filenames = null) { string fileNamePrefix = DefaultFilePathPrefix; string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; @@ -188,9 +186,10 @@ private Project CreateProject(string[] sources, string language = LanguageNames. var solution = this.CreateSolution(projectId, language); int count = 0; - foreach (var source in sources) + for (int i = 0; i < sources.Length; i++) { - var newFileName = fileNamePrefix + count + "." + fileExt; + string source = sources[i]; + var newFileName = filenames?[i] ?? fileNamePrefix + count + "." + fileExt; var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); count++; diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/ExclusionTestAnalyzer.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/ExclusionTestAnalyzer.cs new file mode 100644 index 000000000..ed69662c9 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/ExclusionTestAnalyzer.cs @@ -0,0 +1,48 @@ +namespace TestHelper +{ + using System.Collections.Immutable; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Text; + using StyleCop.Analyzers; + + /// + /// A analyzer that will report a diagnostic at the start of the code file if the + /// file is not excluded from code analysis. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class ExclusionTestAnalyzer : DiagnosticAnalyzer + { + internal const string DiagnosticId = "SA9999"; + private const string Title = "Exclusion test"; + private const string MessageFormat = "Exclusion test"; + private const string Description = "Exclusion test"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, "TestRules", DiagnosticSeverity.Warning, true, Description); + + private static readonly ImmutableArray SupportedDiagnosticsValue = + ImmutableArray.Create(Descriptor); + + /// + public override ImmutableArray SupportedDiagnostics + { + get + { + return SupportedDiagnosticsValue; + } + } + + /// + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxTreeActionHonorExclusions(this.AnalyzeTree); + } + + private void AnalyzeTree(SyntaxTreeAnalysisContext context) + { + // Report a diagnostic if we got called + context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Tree.GetLocation(TextSpan.FromBounds(0, 0)))); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj index 338f23102..db24f891b 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj @@ -146,6 +146,7 @@ + @@ -179,6 +180,7 @@ + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/DiagnosticVerifier.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/DiagnosticVerifier.cs index 436e2aad0..6c014e907 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/DiagnosticVerifier.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/DiagnosticVerifier.cs @@ -35,10 +35,11 @@ public abstract partial class DiagnosticVerifier /// A s describing the that should /// be reported by the analyzer for the specified source. /// The that the task will observe. + /// The filename or null if the default filename should be used /// A representing the asynchronous operation. - protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken) + protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken, string filename = null) { - return this.VerifyCSharpDiagnosticAsync(source, new[] { expected }, cancellationToken); + return this.VerifyCSharpDiagnosticAsync(source, new[] { expected }, cancellationToken, filename); } /// @@ -51,10 +52,11 @@ protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expec /// A collection of s describing the /// s that should be reported by the analyzer for the specified source. /// The that the task will observe. + /// The filename or null if the default filename should be used /// A representing the asynchronous operation. - protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken) + protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken, string filename = null) { - return this.VerifyDiagnosticsAsync(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), expected, cancellationToken); + return this.VerifyDiagnosticsAsync(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), expected, cancellationToken, new[] { filename }); } /// @@ -68,10 +70,11 @@ protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] exp /// A collection of s describing the /// s that should be reported by the analyzer for the specified sources. /// The that the task will observe. + /// The filenames or null if the default filename should be used /// A representing the asynchronous operation. - protected Task VerifyCSharpDiagnosticAsync(string[] sources, DiagnosticResult[] expected, CancellationToken cancellationToken) + protected Task VerifyCSharpDiagnosticAsync(string[] sources, DiagnosticResult[] expected, CancellationToken cancellationToken, string[] filenames = null) { - return this.VerifyDiagnosticsAsync(sources, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), expected, cancellationToken); + return this.VerifyDiagnosticsAsync(sources, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), expected, cancellationToken, filenames); } /// @@ -84,11 +87,28 @@ protected Task VerifyCSharpDiagnosticAsync(string[] sources, DiagnosticResult[] /// A collection of s that should appear after the analyzer /// is run on the sources. /// The that the task will observe. + /// The filenames or null if the default filename should be used /// A representing the asynchronous operation. - private async Task VerifyDiagnosticsAsync(string[] sources, string language, ImmutableArray analyzers, DiagnosticResult[] expected, CancellationToken cancellationToken) + private async Task VerifyDiagnosticsAsync(string[] sources, string language, ImmutableArray analyzers, DiagnosticResult[] expected, CancellationToken cancellationToken, string[] filenames) { - var diagnostics = await this.GetSortedDiagnosticsAsync(sources, language, analyzers, cancellationToken).ConfigureAwait(false); - VerifyDiagnosticResults(diagnostics, analyzers, expected); + VerifyDiagnosticResults(await this.GetSortedDiagnosticsAsync(sources, language, analyzers, cancellationToken, filenames).ConfigureAwait(false), analyzers, expected); + + // If filenames is null we want to test for exclusions too + if (filenames == null) + { + // Also check if the analyzer honors exclusions + if (expected.Any(x => x.Id.StartsWith("SA") || x.Id.StartsWith("SX"))) + { + // We want to look at non-stylecop diagnostics only. We also insert a new line at the beginning + // so we have to move all diagnostic location down by one line + var expectedResults = expected + .Where(x => !x.Id.StartsWith("SA") && !x.Id.StartsWith("SX")) + .Select(x => x.WithLineOffset(1)) + .ToArray(); + + VerifyDiagnosticResults(await this.GetSortedDiagnosticsAsync(sources.Select(x => " // \r\n" + x).ToArray(), language, analyzers, cancellationToken, null).ConfigureAwait(false), analyzers, expectedResults); + } + } } /// @@ -103,7 +123,7 @@ private async Task VerifyDiagnosticsAsync(string[] sources, string language, Imm /// The analyzers that have been run on the sources. /// A collection of s describing the expected /// diagnostics for the sources. - private static void VerifyDiagnosticResults(IEnumerable actualResults, ImmutableArray analyzers, params DiagnosticResult[] expectedResults) + private static void VerifyDiagnosticResults(IEnumerable actualResults, ImmutableArray analyzers, DiagnosticResult[] expectedResults) { int expectedCount = expectedResults.Count(); int actualCount = actualResults.Count(); diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/AnalyzerExtensions.cs b/StyleCop.Analyzers/StyleCop.Analyzers/AnalyzerExtensions.cs index 6ffcdcdba..8d412bf7b 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/AnalyzerExtensions.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/AnalyzerExtensions.cs @@ -8,9 +8,20 @@ namespace StyleCop.Analyzers using System; using Microsoft.CodeAnalysis.Diagnostics; - internal static class AnalyzerExtensions + /// + /// Provides extension methods to deal for analyzers. + /// + public static class AnalyzerExtensions { - internal static void RegisterSyntaxTreeActionHonorExclusions(this AnalysisContext context, Action action) + /// + /// Register an action to be executed at completion of parsing of a code document. + /// A syntax tree action reports Microsoft.CodeAnalysis.Diagnostics about the Microsoft.CodeAnalysis.SyntaxTree + /// of a document. + /// + /// This method honors exclusions + /// The analysis context. + /// Action to be executed at completion of parsing of a document. + public static void RegisterSyntaxTreeActionHonorExclusions(this AnalysisContext context, Action action) { context.RegisterSyntaxTreeAction( c => @@ -28,7 +39,15 @@ internal static void RegisterSyntaxTreeActionHonorExclusions(this AnalysisContex }); } - internal static void RegisterSyntaxTreeActionHonorExclusions(this CompilationStartAnalysisContext context, Action action) + /// + /// Register an action to be executed at completion of parsing of a code document. + /// A syntax tree action reports Microsoft.CodeAnalysis.Diagnostics about the Microsoft.CodeAnalysis.SyntaxTree + /// of a document. + /// + /// This method honors exclusions + /// The analysis context. + /// Action to be executed at completion of parsing of a document. + public static void RegisterSyntaxTreeActionHonorExclusions(this CompilationStartAnalysisContext context, Action action) { context.RegisterSyntaxTreeAction( c => @@ -46,7 +65,18 @@ internal static void RegisterSyntaxTreeActionHonorExclusions(this CompilationSta }); } - internal static void RegisterSyntaxNodeActionHonorExclusions(this AnalysisContext context, Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct + /// + /// Register an action to be executed at completion of semantic analysis of a Microsoft.CodeAnalysis.SyntaxNode + /// with an appropriate Kind. A syntax node action can report Microsoft.CodeAnalysis.Diagnostics + /// about Microsoft.CodeAnalysis.SyntaxNodes, and can also collect state information + /// to be used by other syntax node actions or code block end actions. + /// + /// This method honors exclusions + /// Action will be executed only if a Microsoft.CodeAnalysis.SyntaxNode's Kind matches one of the syntax kind values. + /// Action to be executed at completion of semantic analysis of a Microsoft.CodeAnalysis.SyntaxNode. + /// The kinds of syntax that should be analyzed. + /// Enum type giving the syntax node kinds of the source language for which the action applies. + public static void RegisterSyntaxNodeActionHonorExclusions(this AnalysisContext context, Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct { context.RegisterSyntaxNodeAction( c => @@ -65,7 +95,18 @@ internal static void RegisterSyntaxNodeActionHonorExclusions( syntaxKinds); } - internal static void RegisterSyntaxNodeActionHonorExclusions(this CompilationStartAnalysisContext context, Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct + /// + /// Register an action to be executed at completion of semantic analysis of a Microsoft.CodeAnalysis.SyntaxNode + /// with an appropriate Kind. A syntax node action can report Microsoft.CodeAnalysis.Diagnostics + /// about Microsoft.CodeAnalysis.SyntaxNodes, and can also collect state information + /// to be used by other syntax node actions or code block end actions. + /// + /// This method honors exclusions + /// Action will be executed only if a Microsoft.CodeAnalysis.SyntaxNode's Kind matches one of the syntax kind values. + /// Action to be executed at completion of semantic analysis of a Microsoft.CodeAnalysis.SyntaxNode. + /// The kinds of syntax that should be analyzed. + /// Enum type giving the syntax node kinds of the source language for which the action applies. + public static void RegisterSyntaxNodeActionHonorExclusions(this CompilationStartAnalysisContext context, Action action, params TLanguageKindEnum[] syntaxKinds) where TLanguageKindEnum : struct { context.RegisterSyntaxNodeAction( c => diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1651DoNotUsePlaceholderElements.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1651DoNotUsePlaceholderElements.cs index 106406d01..38394b934 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1651DoNotUsePlaceholderElements.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1651DoNotUsePlaceholderElements.cs @@ -57,8 +57,8 @@ public override ImmutableArray SupportedDiagnostics /// public override void Initialize(AnalysisContext context) { - context.RegisterSyntaxNodeAction(this.HandleXmlElement, SyntaxKind.XmlElement); - context.RegisterSyntaxNodeAction(this.HandleXmlEmptyElement, SyntaxKind.XmlEmptyElement); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleXmlElement, SyntaxKind.XmlElement); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleXmlEmptyElement, SyntaxKind.XmlEmptyElement); } private void HandleXmlElement(SyntaxNodeAnalysisContext context) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/GeneratedCodeAnalysisExtensions.cs b/StyleCop.Analyzers/StyleCop.Analyzers/GeneratedCodeAnalysisExtensions.cs index 76add5410..b5945edf8 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/GeneratedCodeAnalysisExtensions.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/GeneratedCodeAnalysisExtensions.cs @@ -10,6 +10,7 @@ namespace StyleCop.Analyzers using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading; + using Helpers; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -112,7 +113,9 @@ public static bool IsGeneratedDocument(this SyntaxTree tree, CancellationToken c private static bool IsGeneratedDocumentNoCache(SyntaxTree tree, CancellationToken cancellationToken) { - return IsGeneratedFileName(tree.FilePath) || HasAutoGeneratedComment(tree, cancellationToken); + return IsGeneratedFileName(tree.FilePath) + || HasAutoGeneratedComment(tree, cancellationToken) + || IsEmpty(tree, cancellationToken); } /// @@ -179,5 +182,28 @@ private static bool IsGeneratedFileName(string filePath) @"\.designer\.cs$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); } + + /// + /// Checks if a given only contains whitespaces. We don't want to analyze empty files. + /// + /// The syntax tree to examine. + /// The that the task will observe. + /// + /// if only contains whitespaces; otherwise, . + /// + private static bool IsEmpty(SyntaxTree tree, CancellationToken cancellationToken) + { + var root = tree.GetRoot(cancellationToken); + + if (root == null) + { + return false; + } + + var firstToken = root.GetFirstToken(includeZeroWidth: true); + + return firstToken.IsKind(SyntaxKind.EndOfFileToken) + && TriviaHelper.IndexOfFirstNonWhitespaceTrivia(firstToken.LeadingTrivia) == -1; + } } }