Skip to content

Commit f0d386b

Browse files
authored
Add rule url resolver for Stylelint (#986)
1 parent facaaf1 commit f0d386b

File tree

7 files changed

+213
-7
lines changed

7 files changed

+213
-7
lines changed

docs/input/documentation/issue-providers/tap/features.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The [Cake.Issues.Tap addin](https://cakebuild.net/extensions/cake-issues-tap/){t
2020

2121
- [x] [GenericLogFileFormat]{target="_blank"} alias for reading issues from any [Test Anything Protocol (TAP)]{target="_blank"} compatible file
2222
- [x] [StylelintLogFileFormat]{target="_blank"} alias for reading TAP files generated by [stylelint](https://stylelint.io/){target="_blank"}.
23+
- [x] Provides URLs for rules shipped with stylelint.
24+
- [x] Support for custom URL resolving using the `TapStylelintAddRuleUrlResolver` alias.
2325
- [x] [TextlintLogFileFormat]{target="_blank"} alias for reading TAP files generated by [Textlint](https://textlint.github.io/){target="_blank"}.
2426

2527
## Supported IIssue properties

src/Cake.Issues.Tap.Tests/LogFileFormat/StylelintLogFileFormatTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void Should_Read_Issue_Correct_When_Warnings()
5353
"TAP")
5454
.InFile("path/to/file1.css", 2, 3, 1, 3)
5555
.WithPriority(IssuePriority.Error)
56-
.OfRule("baz")
56+
.OfRule("baz", new Uri("https://stylelint.io/user-guide/rules/baz"))
5757
.Create());
5858

5959
issue = issues[1];
@@ -65,7 +65,7 @@ public void Should_Read_Issue_Correct_When_Warnings()
6565
"TAP")
6666
.InFile("path/to/file2.css", 1, 2, 1, 3)
6767
.WithPriority(IssuePriority.Error)
68-
.OfRule("bar")
68+
.OfRule("bar", new Uri("https://stylelint.io/user-guide/rules/bar"))
6969
.Create());
7070

7171
issue = issues[2];
@@ -77,7 +77,7 @@ public void Should_Read_Issue_Correct_When_Warnings()
7777
"TAP")
7878
.InFile("path/to/file2.css", 4, 5, 1, 3)
7979
.WithPriority(IssuePriority.Error)
80-
.OfRule("bar")
80+
.OfRule("bar", new Uri("https://stylelint.io/user-guide/rules/bar"))
8181
.Create());
8282

8383
issue = issues[3];
@@ -89,7 +89,7 @@ public void Should_Read_Issue_Correct_When_Warnings()
8989
"TAP")
9090
.InFile("path/to/file2.css", 10, 11, 1, 2)
9191
.WithPriority(IssuePriority.Error)
92-
.OfRule("bar2")
92+
.OfRule("bar2", new Uri("https://stylelint.io/user-guide/rules/bar2"))
9393
.Create());
9494
}
9595

@@ -184,7 +184,7 @@ public void Should_Read_Issue_Correct_When_Absolute_Windows_Path(string reposito
184184
"TAP")
185185
.InFile(relativeFilePath, 16, 16, 6, 8)
186186
.WithPriority(IssuePriority.Error)
187-
.OfRule("block-no-empty")
187+
.OfRule("block-no-empty", new Uri("https://stylelint.io/user-guide/rules/block-no-empty"))
188188
.Create());
189189
}
190190

@@ -213,7 +213,7 @@ public void Should_Read_Issue_Correct_When_Absolute_Linux_Path(string repository
213213
"TAP")
214214
.InFile(relativeFilePath, 16, 16, 6, 8)
215215
.WithPriority(IssuePriority.Error)
216-
.OfRule("block-no-empty")
216+
.OfRule("block-no-empty", new Uri("https://stylelint.io/user-guide/rules/block-no-empty"))
217217
.Create());
218218
}
219219

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
namespace Cake.Issues.Tap.Tests.LogFileFormat;
2+
3+
using Cake.Issues.Tap.LogFileFormat;
4+
5+
public sealed class StylelintRuleUrlResolverTests
6+
{
7+
public sealed class TheResolveRuleUrlMethod
8+
{
9+
[Fact]
10+
public void Should_Throw_If_Rule_Is_Null()
11+
{
12+
// Given / When
13+
var result = Record.Exception(() => StylelintRuleUrlResolver.Instance.ResolveRuleUrl(null));
14+
15+
// Then
16+
result.IsArgumentNullException("rule");
17+
}
18+
19+
[Fact]
20+
public void Should_Throw_If_Rule_Is_Empty()
21+
{
22+
// Given / When
23+
var result = Record.Exception(() => StylelintRuleUrlResolver.Instance.ResolveRuleUrl(string.Empty));
24+
25+
// Then
26+
result.IsArgumentOutOfRangeException("rule");
27+
}
28+
29+
[Fact]
30+
public void Should_Throw_If_Rule_Is_WhiteSpace()
31+
{
32+
// Given / When
33+
var result = Record.Exception(() => StylelintRuleUrlResolver.Instance.ResolveRuleUrl(" "));
34+
35+
// Then
36+
result.IsArgumentOutOfRangeException("rule");
37+
}
38+
39+
[Theory]
40+
[InlineData("block-no-empty", "https://stylelint.io/user-guide/rules/block-no-empty")]
41+
public void Should_Resolve_Url(string rule, string expectedUrl)
42+
{
43+
// Given
44+
var urlResolver = StylelintRuleUrlResolver.Instance;
45+
46+
// When
47+
var ruleUrl = urlResolver.ResolveRuleUrl(rule);
48+
49+
// Then
50+
ruleUrl.ToString().ShouldBe(expectedUrl);
51+
}
52+
53+
[Theory]
54+
[InlineData("csstools/use-logical")]
55+
public void Should_Not_Resolve_Url_When_Issue_From_Plugin(string rule)
56+
{
57+
// Given
58+
var urlResolver = StylelintRuleUrlResolver.Instance;
59+
60+
// When
61+
var ruleUrl = urlResolver.ResolveRuleUrl(rule);
62+
63+
// Then
64+
ruleUrl.ShouldBeNull();
65+
}
66+
67+
[Fact]
68+
public void Should_Resolve_Url_From_Custom_Resolvers()
69+
{
70+
// Given
71+
const string foo = "FOO123";
72+
const string fooUrl = "http://foo.com/";
73+
const string bar = "BAR123";
74+
const string barUrl = "http://bar.com/";
75+
var urlResolver = StylelintRuleUrlResolver.Instance;
76+
urlResolver.AddUrlResolver(x => x.Rule == foo ? new Uri(fooUrl) : null, 1);
77+
urlResolver.AddUrlResolver(x => x.Rule == bar ? new Uri(barUrl) : null, 1);
78+
79+
// When
80+
var fooRuleUrl = urlResolver.ResolveRuleUrl(foo);
81+
var barRuleUrl = urlResolver.ResolveRuleUrl(bar);
82+
83+
// Then
84+
fooRuleUrl.ToString().ShouldBe(fooUrl);
85+
barRuleUrl.ToString().ShouldBe(barUrl);
86+
}
87+
}
88+
}

src/Cake.Issues.Tap/LogFileFormat/StylelintLogFileFormat.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public override IEnumerable<IIssue> ReadIssues(
5454
diagnosticDict.ContainsKey("endLine") ? int.Parse(diagnosticDict["endLine"].ToString()) : null,
5555
diagnosticDict.ContainsKey("column") ? int.Parse(diagnosticDict["column"].ToString()) : null,
5656
diagnosticDict.ContainsKey("endColumn") ? int.Parse(diagnosticDict["endColumn"].ToString()) : null)
57-
.OfRule(diagnosticKey)
57+
.OfRule(diagnosticKey, StylelintRuleUrlResolver.Instance.ResolveRuleUrl(diagnosticKey))
5858
.WithPriority(GetPriority(diagnosticDict["severity"].ToString()));
5959
result.Add(issueBuilder.Create());
6060
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Cake.Issues.Tap.LogFileFormat;
2+
3+
/// <summary>
4+
/// Class describing rules appearing in TAP logs written by stylelint.
5+
/// </summary>
6+
public class StylelintRuleDescription : BaseRuleDescription
7+
{
8+
/// <summary>
9+
/// Gets or sets a value indicating whether the rule is from a plugin or not.
10+
/// </summary>
11+
public bool IsPlugin { get; set; }
12+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace Cake.Issues.Tap.LogFileFormat;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
6+
/// <summary>
7+
/// Class for retrieving a URL linking to a site describing a rule.
8+
/// </summary>
9+
internal class StylelintRuleUrlResolver : BaseRuleUrlResolver<StylelintRuleDescription>
10+
{
11+
private static readonly Lazy<StylelintRuleUrlResolver> InstanceValue =
12+
new(() => new StylelintRuleUrlResolver());
13+
14+
private readonly IList<string> rulesWithoutDocumentation = ["parseError", "unknown"];
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="StylelintRuleUrlResolver"/> class.
18+
/// </summary>
19+
private StylelintRuleUrlResolver()
20+
{
21+
this.AddUrlResolver(x =>
22+
!x.IsPlugin && !this.rulesWithoutDocumentation.Contains(x.Rule) ?
23+
new Uri("https://stylelint.io/user-guide/rules/" + x.Rule) :
24+
null);
25+
}
26+
27+
/// <summary>
28+
/// Gets the instance of the rule resolver.
29+
/// </summary>
30+
public static StylelintRuleUrlResolver Instance => InstanceValue.Value;
31+
32+
/// <inheritdoc/>
33+
protected override bool TryGetRuleDescription(string rule, StylelintRuleDescription ruleDescription)
34+
{
35+
ruleDescription.IsPlugin = rule.Contains('/');
36+
37+
return true;
38+
}
39+
}

src/Cake.Issues.Tap/TapIssuesAliases.StylelintLogFileFormat.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Cake.Issues.Tap;
22

3+
using System;
34
using Cake.Core;
45
using Cake.Core.Annotations;
56
using Cake.Issues.Tap.LogFileFormat;
@@ -23,4 +24,68 @@ public static BaseTapLogFileFormat StylelintLogFileFormat(
2324

2425
return new StylelintLogFileFormat(context.Log);
2526
}
27+
28+
/// <summary>
29+
/// Registers a new URL resolver with default priority of 0.
30+
/// </summary>
31+
/// <param name="context">The context.</param>
32+
/// <param name="resolver">Resolver which returns an <see cref="Uri"/> linking to a site
33+
/// containing help for a specific <see cref="BaseRuleDescription"/>.</param>
34+
/// <example>
35+
/// <para>Adds a provider with default priority of 0 returning a link for all rules starting
36+
/// with the string <c>Foo</c> to search with Google for the rule:</para>
37+
/// <code>
38+
/// <![CDATA[
39+
/// TapStylelintAddRuleUrlResolver(x =>
40+
/// x.Rule.StartsWith("Foo") ?
41+
/// new Uri("https://www.google.com/search?q=%22" + x.Rule + "%22") :
42+
/// null)
43+
/// ]]>
44+
/// </code>
45+
/// </example>
46+
[CakeMethodAlias]
47+
[CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
48+
public static void TapStylelintAddRuleUrlResolver(
49+
this ICakeContext context,
50+
Func<BaseRuleDescription, Uri> resolver)
51+
{
52+
context.NotNull();
53+
resolver.NotNull();
54+
55+
StylelintRuleUrlResolver.Instance.AddUrlResolver(resolver);
56+
}
57+
58+
/// <summary>
59+
/// Registers a new URL resolver with a specific priority.
60+
/// </summary>
61+
/// <param name="context">The context.</param>
62+
/// <param name="resolver">Resolver which returns an <see cref="Uri"/> linking to a site
63+
/// containing help for a specific <see cref="BaseRuleDescription"/>.</param>
64+
/// <param name="priority">Priority of the resolver. Resolver with a higher priority are considered
65+
/// first during resolving of the URL.</param>
66+
/// <example>
67+
/// <para>Adds a provider of priority 5 returning a link for all rules starting with the string
68+
/// <c>Foo</c> to search with Google for the rule:</para>
69+
/// <code>
70+
/// <![CDATA[
71+
/// TapStylelintAddRuleUrlResolver(x =>
72+
/// x.Rule.StartsWith("Foo") ?
73+
/// new Uri("https://www.google.com/search?q=%22" + x.Rule + "%22") :
74+
/// null,
75+
/// 5)
76+
/// ]]>
77+
/// </code>
78+
/// </example>
79+
[CakeMethodAlias]
80+
[CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)]
81+
public static void TapStylelintAddRuleUrlResolver(
82+
this ICakeContext context,
83+
Func<BaseRuleDescription, Uri> resolver,
84+
int priority)
85+
{
86+
context.NotNull();
87+
resolver.NotNull();
88+
89+
StylelintRuleUrlResolver.Instance.AddUrlResolver(resolver, priority);
90+
}
2691
}

0 commit comments

Comments
 (0)