Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:nsubstitute/NSubstitute.Analyzers into
Browse files Browse the repository at this point in the history
GH-35-arg-matcher
  • Loading branch information
tpodolak committed Oct 18, 2019
2 parents ffd19f2 + 46f8213 commit 1961407
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 24 deletions.
10 changes: 8 additions & 2 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
29 changes: 27 additions & 2 deletions build/build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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";
Expand Down
30 changes: 30 additions & 0 deletions build/table-of-contents.cake
Original file line number Diff line number Diff line change
@@ -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 = @"<td>CheckId<\/td>\s*<td>(?<value>[\w\s]+)<\/td>";
private static string CategoryPattern = @"<td>Category<\/td>\s*<td>(?<value>[\w\s\-]+)<\/td>";
private static string DescriptionPattern = @"## Cause\s+(?<value>[^#]+)\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}";
}
2 changes: 1 addition & 1 deletion documentation/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### Rules

See [rules list](rules).
See [rules list](rules/README.md).

### Additional documentation

Expand Down
26 changes: 26 additions & 0 deletions documentation/rules/README.md
Original file line number Diff line number Diff line change
@@ -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. |
Original file line number Diff line number Diff line change
@@ -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<object[]> DiagnosticDescriptors { get; } = DiagnosticIdentifierTests.DiagnosticIdentifierTests.DiagnosticDescriptors.Select(diag => new object[] { diag }).ToList();
private static readonly ImmutableArray<DiagnosticDescriptor> 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<object[]> 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);
Expand All @@ -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);
Expand All @@ -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<HeadingContainer> parsedDocumentation)
{
var ruleRowLocation = DiagnosticDescriptors.IndexOf(descriptor);
var rulesTable = parsedDocumentation.Single(container => GetBlockText(container.Heading) == "Rules")
.Children.OfType<Table>().Single();

// skip header row
var ruleRow = rulesTable.OfType<TableRow>().Skip(1).ElementAt(ruleRowLocation);
var cells = ruleRow.OfType<TableCell>().ToList();
AssertRuleSummaryIdCell(cells.First(), descriptor);
AssertRuleSummaryCategoryCell(cells.ElementAt(1), descriptor);
AssertRuleSummaryCauseCell(cells.ElementAt(2), descriptor);
}

private static List<Block> GetParsedDocumentation(DiagnosticDescriptor descriptor)
{
var directoryName = GetRulesDocumentationDirectoryPath();
Expand Down Expand Up @@ -104,11 +139,10 @@ private void AssertHeadingsLayout(List<HeadingContainer> 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<HeadingContainer> layout, string ruleId, string ruleCategory)
Expand Down Expand Up @@ -140,19 +174,38 @@ private void AssertTableContent(List<HeadingContainer> layout, string ruleId, st
children.Single().As<HtmlBlock>().Lines.ToString().Should().Be(expectedInfo);
}

private static IEnumerable<T> Traverse<T>(
IEnumerable<T> items,
Func<T, IEnumerable<T>> childSelector)
private void AssertRuleSummaryIdCell(TableCell cell, DiagnosticDescriptor descriptor)
{
var descendants = cell.Descendants().ToList();
var linkInline = descendants.OfType<LinkInline>().Single();
linkInline.Url.Should().Be($"{descriptor.Id}.md");
linkInline.FirstChild.ToString().Should().Be(descriptor.Id);
}

private void AssertRuleSummaryCategoryCell(TableCell cell, DiagnosticDescriptor descriptor)
{
cell.OfType<ParagraphBlock>().Single().Inline.Single().ToString().Should().Be(descriptor.Category);
}

private void AssertRuleSummaryCauseCell(TableCell cell, DiagnosticDescriptor descriptor)
{
var stack = new Stack<T>(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<ParagraphBlock>().Single());
var ruleContent = GetBlockText(headingContainer.Children.OfType<ParagraphBlock>().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();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="2.6.0" />
<PackageReference Include="Markdig" Version="0.15.4" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="2.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="2.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.8.2" />
<PackageReference Include="morelinq" Version="3.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="2.8.2" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="2.8.2" />
<PackageReference Include="NSubstitute" Version="4.0.0" />
<PackageReference Include="FluentAssertions" Version="5.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
Expand Down

0 comments on commit 1961407

Please sign in to comment.