diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 4b51e220..236d8543 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,10 @@ +### 1.0.11 (15 October 2019) + +- [#124](https://github.com/nsubstitute/NSubstitute.Analyzers/issues/124) - NS4000 false positive when using an initializer +- [#118](https://github.com/nsubstitute/NSubstitute.Analyzers/issues/118) - NS3002 false positive +- [#115](https://github.com/nsubstitute/NSubstitute.Analyzers/issues/115) - Eliminate unnecessary allocations and improve performance +- [#106](https://github.com/nsubstitute/NSubstitute.Analyzers/issues/106) - NS4000 false positive in foreach loop when element used twice + ### 1.0.10 (5 June 2019) - [#103](https://github.com/nsubstitute/NSubstitute.Analyzers/issues/103) - Enable concurrent execution for every analyzer @@ -82,5 +89,4 @@ - [#1](https://github.com/nsubstitute/NSubstitute.Analyzers/issues/1) - Checking constructor arguments passed via `Substitute.For`/`Substitute.ForPartsOf` ### 0.1.0-beta1 (01 August 2018) - - Initial release - + - Initial release \ No newline at end of file diff --git a/build/build.cake b/build/build.cake index 33eb0a88..0d5a1c3e 100644 --- a/build/build.cake +++ b/build/build.cake @@ -2,6 +2,7 @@ #load "./version.cake" #load "./paths.cake" #load "./releasenotes.cake" +#load "./table-of-contents.cake" // Install modules #module nuget:?package=Cake.DotNetTool.Module&version=0.1.0 @@ -14,12 +15,12 @@ #addin "nuget:https://www.nuget.org/api/v2?package=Newtonsoft.Json&version=9.0.1" #addin "nuget:https://www.nuget.org/api/v2?package=semver.core&version=2.0.0" +using Cake.Incubator.LoggingExtensions; using Newtonsoft.Json.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.RegularExpressions; -using Cake.Incubator.LoggingExtensions; using System.Threading; -using System.Net.Http.Headers; var parameters = BuildParameters.GetParameters(Context); var buildVersion = BuildVersion.Calculate(Context); @@ -291,6 +292,30 @@ Task("AppVeyor") .IsDependentOn("Upload-Coverage-Report") .IsDependentOn("Publish"); +Task("GenerateDocTableOfContents") + .Does(() => +{ + var rulesDir = paths.Directories.RootDir.Combine("documentation").Combine("rules"); + var header = @" +## Rules + +| ID | Category | Cause | +|---|---|---| +"; + Information("Generating Table of Contents for {0}", rulesDir); + var entries = + GetFiles($"{rulesDir}/NS*.md") + .Select(TableOfContentsEntry.Parse) + .OrderBy(entry => entry.CheckId); + var contents = header + string.Join("\n", + entries.Select(entry => $"| [{entry.CheckId}]({entry.CheckId}.md) | {entry.Category} | {entry.Description} |") + ); + + var target = $"{rulesDir}/README.md"; + System.IO.File.WriteAllText(target, contents); + Information("Generated Table of Context: {0}", target); +}); + Teardown(context => { var result = context.Successful ? "succeeded" : "failed"; diff --git a/build/table-of-contents.cake b/build/table-of-contents.cake new file mode 100644 index 00000000..8ddb4ccb --- /dev/null +++ b/build/table-of-contents.cake @@ -0,0 +1,30 @@ +public class TableOfContentsEntry { + + public string CheckId { get; } + public string Category { get; } + public string Description { get; } + public FilePath FilePath { get; } + + private static string CheckIdPattern = @"CheckId<\/td>\s*(?[\w\s]+)<\/td>"; + private static string CategoryPattern = @"Category<\/td>\s*(?[\w\s\-]+)<\/td>"; + private static string DescriptionPattern = @"## Cause\s+(?[^#]+)\s+##"; + + private TableOfContentsEntry(string checkId, string category, string description, FilePath file) + { + CheckId = checkId; + Category = category; + Description = description; + FilePath = file; + } + + public static TableOfContentsEntry Parse(FilePath file) + { + var s = System.IO.File.ReadAllText(file.ToString()); + var checkId = Regex.Match(s, CheckIdPattern).Groups["value"].Value.Trim(); + var category = Regex.Match(s, CategoryPattern).Groups["value"].Value.Trim(); + var description = Regex.Match(s, DescriptionPattern).Groups["value"].Value.Trim(); + return new TableOfContentsEntry(checkId, category, description, file); + } + + public override string ToString() => $"{CheckId}: {Category}\n{Description}"; +} \ No newline at end of file diff --git a/documentation/README.md b/documentation/README.md index a2034c48..da2d67e6 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -1,6 +1,6 @@ ### Rules -See [rules list](rules). +See [rules list](rules/README.md). ### Additional documentation diff --git a/documentation/rules/README.md b/documentation/rules/README.md new file mode 100644 index 00000000..8398904e --- /dev/null +++ b/documentation/rules/README.md @@ -0,0 +1,26 @@ + +## Rules + +| ID | Category | Cause | +|---|---|---| +| [NS1000](NS1000.md) | Non-substitutable member | Substituting for non-virtual member of a class. | +| [NS1001](NS1001.md) | Non-substitutable member | Checking received calls for non-virtual member of a class. | +| [NS1002](NS1002.md) | Non-substitutable member | Substituting for non-virtual member of a class. | +| [NS1003](NS1003.md) | Non-substitutable member | Substituting for an internal member of a class without proxies having visibility into internal members. | +| [NS2000](NS2000.md) | Substitute creation | Substitute.ForPartsOf used with interface or delegate. | +| [NS2001](NS2001.md) | Substitute creation | NSubstitute used with class which does not expose public or protected constructor. | +| [NS2002](NS2002.md) | Substitute creation | NSubstitute used with class which does not expose parameterless constructor. | +| [NS2003](NS2003.md) | Substitute creation | NSubstitute used with internal type. | +| [NS2004](NS2004.md) | Substitute creation | Substituting for type by passing wrong constructor arguments. | +| [NS2005](NS2005.md) | Substitute creation | Substituting for multiple classes. | +| [NS2006](NS2006.md) | Substitute creation | Substituting for interface and passing arguments. | +| [NS2007](NS2007.md) | Substitute creation | Substituting for delegate and passing arguments. | +| [NS3000](NS3000.md) | Argument specification | Accessing call arguments out of the bounds of method arguments. | +| [NS3001](NS3001.md) | Argument specification | Casting call argument at given position to different type than type specified in a method. | +| [NS3002](NS3002.md) | Argument specification | Accessing call argument by type which is not present in invocation. | +| [NS3003](NS3003.md) | Argument specification | Accessing call argument by type which is used multiple times in invocation. | +| [NS3004](NS3004.md) | Argument specification | Assigning call argument with type which is not the same as method argument type. | +| [NS3005](NS3005.md) | Argument specification | Assigning call argument which is not ref nor out argument. | +| [NS3006](NS3006.md) | Argument specification | Conflicting assignments to out/ref arguments. | +| [NS4000](NS4000.md) | Call configuration | Calling substitute from within `Returns` block. | +| [NS5000](NS5000.md) | Usage | Checking received calls without specifying member. | \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/DocumentationTests/DocumentationTests.cs b/tests/NSubstitute.Analyzers.Tests.Shared/DocumentationTests/DocumentationTests.cs index 392e2ee6..b7f75ad1 100644 --- a/tests/NSubstitute.Analyzers.Tests.Shared/DocumentationTests/DocumentationTests.cs +++ b/tests/NSubstitute.Analyzers.Tests.Shared/DocumentationTests/DocumentationTests.cs @@ -1,23 +1,33 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; using FluentAssertions; using Markdig; +using Markdig.Extensions.Tables; +using Markdig.Renderers.Normalize; using Markdig.Syntax; using Markdig.Syntax.Inlines; using Microsoft.CodeAnalysis; +using MoreLinq.Extensions; using Xunit; namespace NSubstitute.Analyzers.Tests.Shared.DocumentationTests { public class DocumentationTests { - public static IEnumerable DiagnosticDescriptors { get; } = DiagnosticIdentifierTests.DiagnosticIdentifierTests.DiagnosticDescriptors.Select(diag => new object[] { diag }).ToList(); + private static readonly ImmutableArray DiagnosticDescriptors = DiagnosticIdentifierTests + .DiagnosticIdentifierTests.DiagnosticDescriptors + .DistinctBy(diag => diag.Id) // NS5000 is duplicated as it is used in two flavours extension/non-extension method usage + .OrderBy(diag => diag.Id).ToImmutableArray(); + + public static IEnumerable DiagnosticDescriptorTestCases { get; } = DiagnosticDescriptors + .Select(diag => new object[] { diag }).ToList(); [Theory] - [MemberData(nameof(DiagnosticDescriptors))] + [MemberData(nameof(DiagnosticDescriptorTestCases))] public void DiagnosticDocumentation_ShouldHave_ProperHeadings(DiagnosticDescriptor descriptor) { var markdownDocument = GetParsedDocumentation(descriptor); @@ -29,7 +39,7 @@ public void DiagnosticDocumentation_ShouldHave_ProperHeadings(DiagnosticDescript } [Theory] - [MemberData(nameof(DiagnosticDescriptors))] + [MemberData(nameof(DiagnosticDescriptorTestCases))] public void DiagnosticDocumentation_ShouldHave_ProperContent(DiagnosticDescriptor descriptor) { var markdownDocument = GetParsedDocumentation(descriptor); @@ -40,6 +50,31 @@ public void DiagnosticDocumentation_ShouldHave_ProperContent(DiagnosticDescripto AssertContent(layout, descriptor.Id, descriptor.Category); } + [Theory] + [MemberData(nameof(DiagnosticDescriptorTestCases))] + public void RulesSummary_ShouldHave_ContentCorrespondingToRuleFile(DiagnosticDescriptor descriptor) + { + var documentationDirectory = GetRulesDocumentationDirectoryPath(); + var rulesSummaryFileInfo = new FileInfo(Path.Combine(documentationDirectory, "README.md")); + var parsedDocumentation = GetLayoutByHeadings(GetParsedDocumentation(rulesSummaryFileInfo)); + + AssertRulesSummaryRow(descriptor, parsedDocumentation); + } + + private void AssertRulesSummaryRow(DiagnosticDescriptor descriptor, List parsedDocumentation) + { + var ruleRowLocation = DiagnosticDescriptors.IndexOf(descriptor); + var rulesTable = parsedDocumentation.Single(container => GetBlockText(container.Heading) == "Rules") + .Children.OfType().Single(); + + // skip header row + var ruleRow = rulesTable.OfType().Skip(1).ElementAt(ruleRowLocation); + var cells = ruleRow.OfType().ToList(); + AssertRuleSummaryIdCell(cells.First(), descriptor); + AssertRuleSummaryCategoryCell(cells.ElementAt(1), descriptor); + AssertRuleSummaryCauseCell(cells.ElementAt(2), descriptor); + } + private static List GetParsedDocumentation(DiagnosticDescriptor descriptor) { var directoryName = GetRulesDocumentationDirectoryPath(); @@ -104,11 +139,10 @@ private void AssertHeadingsLayout(List layout, string ruleId) private void AssertHeading(HeadingBlock heading, int expectedLevel, string expectedText) { - var inline = heading.Inline.ToList(); + var headingText = GetBlockText(heading); - inline.Should().HaveCount(1); heading.Level.Should().Be(expectedLevel); - inline[0].ToString().Should().Be(expectedText); + headingText.Should().Be(expectedText); } private void AssertContent(List layout, string ruleId, string ruleCategory) @@ -140,19 +174,38 @@ private void AssertTableContent(List layout, string ruleId, st children.Single().As().Lines.ToString().Should().Be(expectedInfo); } - private static IEnumerable Traverse( - IEnumerable items, - Func> childSelector) + private void AssertRuleSummaryIdCell(TableCell cell, DiagnosticDescriptor descriptor) + { + var descendants = cell.Descendants().ToList(); + var linkInline = descendants.OfType().Single(); + linkInline.Url.Should().Be($"{descriptor.Id}.md"); + linkInline.FirstChild.ToString().Should().Be(descriptor.Id); + } + + private void AssertRuleSummaryCategoryCell(TableCell cell, DiagnosticDescriptor descriptor) + { + cell.OfType().Single().Inline.Single().ToString().Should().Be(descriptor.Category); + } + + private void AssertRuleSummaryCauseCell(TableCell cell, DiagnosticDescriptor descriptor) { - var stack = new Stack(items); - while (stack.Any()) + var ruleDocument = GetParsedDocumentation(descriptor); + var layoutByHeadings = GetLayoutByHeadings(ruleDocument); + var headingContainer = layoutByHeadings.Single(heading => GetBlockText(heading.Heading) == "Cause"); + + var cellContent = GetBlockText(cell.OfType().Single()); + var ruleContent = GetBlockText(headingContainer.Children.OfType().Single()); + + cellContent.Should().Be(ruleContent); + } + + private static string GetBlockText(LeafBlock heading) + { + using (var stringWriter = new StringWriter()) { - var next = stack.Pop(); - yield return next; - foreach (var child in childSelector(next)) - { - stack.Push(child); - } + var renderer = new NormalizeRenderer(stringWriter); + renderer.Write(heading.Inline); + return stringWriter.ToString(); } } diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/NSubstitute.Analyzers.Tests.Shared.csproj b/tests/NSubstitute.Analyzers.Tests.Shared/NSubstitute.Analyzers.Tests.Shared.csproj index 0b574178..7fe784f8 100644 --- a/tests/NSubstitute.Analyzers.Tests.Shared/NSubstitute.Analyzers.Tests.Shared.csproj +++ b/tests/NSubstitute.Analyzers.Tests.Shared/NSubstitute.Analyzers.Tests.Shared.csproj @@ -9,8 +9,10 @@ - - + + + +