Skip to content

Commit b3bd1a6

Browse files
author
Per Kops
committed
fix(providers): update MicrosoftCompilerErrorsProvider for Learn site restructure
- Replace TOC JSON parsing with hardcoded category page paths - Add trailing slash to DocumentationLink for correct Uri construction - Parse CS codes from category pages instead of individual error pages - Handle new documentation structure where errors are grouped by category
1 parent 7f150e7 commit b3bd1a6

File tree

1 file changed

+98
-86
lines changed

1 file changed

+98
-86
lines changed

src/Atc.CodingRules.AnalyzerProviders/Providers/MicrosoftCompilerErrorsProvider.cs

Lines changed: 98 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,126 +13,138 @@ public MicrosoftCompilerErrorsProvider(
1313

1414
public static string Name => "Microsoft.CompilerErrors";
1515

16-
public override Uri? DocumentationLink { get; set; } = new("https://learn.microsoft.com/en-us/dotnet/csharp/language-reference", UriKind.Absolute);
16+
public override Uri? DocumentationLink { get; set; } = new("https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/", UriKind.Absolute);
1717

1818
protected override AnalyzerProviderBaseRuleData CreateData()
1919
=> new(Name);
2020

21-
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
2221
protected override async Task ReCollect(AnalyzerProviderBaseRuleData data)
2322
{
2423
ArgumentNullException.ThrowIfNull(data);
2524

26-
var web = new HtmlWeb();
27-
var htmlDoc = await web
28-
.LoadFromWebAsync(DocumentationLink!.AbsoluteUri + "/toc.json")
29-
.ConfigureAwait(false);
30-
31-
if (htmlDoc.DocumentNode.HasTitleWithAccessDenied())
25+
var categoryPages = new[]
26+
{
27+
"compiler-messages/preprocessor-errors",
28+
"compiler-messages/attribute-usage-errors",
29+
"compiler-messages/feature-version-errors",
30+
"compiler-messages/assembly-references",
31+
"compiler-messages/constructor-errors",
32+
"compiler-messages/overloaded-operator-errors",
33+
"compiler-messages/parameter-argument-mismatch",
34+
"compiler-messages/generic-type-parameters-errors",
35+
"compiler-messages/async-await-errors",
36+
"compiler-messages/interface-implementation-errors",
37+
"compiler-messages/ref-modifiers-errors",
38+
"compiler-messages/ref-safety-errors",
39+
"compiler-messages/ref-struct-errors",
40+
"compiler-messages/iterator-yield",
41+
"compiler-messages/extension-declarations",
42+
"compiler-messages/partial-declarations",
43+
"compiler-messages/params-arrays",
44+
"compiler-messages/nullable-warnings",
45+
"compiler-messages/pattern-matching-warnings",
46+
"compiler-messages/string-literal",
47+
"compiler-messages/array-declaration-errors",
48+
"compiler-messages/inline-array-errors",
49+
"compiler-messages/lambda-expression-errors",
50+
"compiler-messages/overload-resolution",
51+
"compiler-messages/expression-tree-restrictions",
52+
"compiler-messages/using-directive-errors",
53+
"compiler-messages/using-statement-declaration-errors",
54+
"compiler-messages/source-generator-errors",
55+
"compiler-messages/static-abstract-interfaces",
56+
"compiler-messages/lock-semantics",
57+
"compiler-messages/dynamic-type-and-binding-errors",
58+
"compiler-messages/unsafe-code-errors",
59+
"compiler-messages/warning-waves",
60+
};
61+
62+
foreach (var categoryPath in categoryPages)
3263
{
33-
data.ExceptionMessage = "Access Denied";
34-
return;
64+
var categoryUri = new Uri(DocumentationLink!, categoryPath);
65+
var rules = await GetRulesFromCategoryPage(categoryUri.AbsoluteUri);
66+
foreach (var rule in rules)
67+
{
68+
data.Rules.Add(rule);
69+
}
3570
}
71+
}
3672

37-
var jsonDoc = JsonDocument.Parse(htmlDoc.DocumentNode.InnerText);
38-
var jsonDocItems = jsonDoc.RootElement
39-
.GetProperty("items")
40-
.EnumerateArray();
73+
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
74+
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "OK - graceful degradation for individual category pages.")]
75+
private static async Task<List<Rule>> GetRulesFromCategoryPage(
76+
string categoryUrl)
77+
{
78+
var rules = new List<Rule>();
79+
var web = new HtmlWeb();
4180

42-
while (jsonDocItems.MoveNext())
81+
try
4382
{
44-
var jsonElement = jsonDocItems.Current;
83+
var htmlDoc = await web
84+
.LoadFromWebAsync(categoryUrl)
85+
.ConfigureAwait(false);
4586

46-
if (!jsonElement.GetRawText().Contains("children", StringComparison.Ordinal))
87+
if (htmlDoc.DocumentNode.HasTitleWithAccessDenied())
4788
{
48-
continue;
89+
return rules;
4990
}
5091

51-
var tocTitle = jsonElement
52-
.GetProperty("toc_title")
53-
.ToString();
54-
55-
if (!tocTitle.Equals("C# compiler messages", StringComparison.Ordinal))
92+
var mainNode = htmlDoc.DocumentNode.SelectSingleNode("//main[@id='main']");
93+
if (mainNode is null)
5694
{
57-
continue;
95+
return rules;
5896
}
5997

60-
var jsonChildItems = jsonElement
61-
.GetProperty("children")
62-
.EnumerateArray();
98+
// Find all strong elements that contain CS error codes
99+
var strongNodes = mainNode.SelectNodes(".//strong");
100+
if (strongNodes is null)
101+
{
102+
return rules;
103+
}
63104

64-
while (jsonChildItems.MoveNext())
105+
foreach (var strongNode in strongNodes)
65106
{
66-
var jsonChildElement = jsonChildItems.Current;
67-
if (jsonChildElement.ValueKind != JsonValueKind.Object ||
68-
!jsonChildElement.TryGetProperty("children", out var jsonChildElement2))
107+
var text = strongNode.InnerText.Trim();
108+
if (!text.StartsWith("CS", StringComparison.Ordinal))
69109
{
70110
continue;
71111
}
72112

73-
foreach (var element in jsonChildElement2.EnumerateArray())
113+
// Extract just the CS code (e.g., "CS1024" from "CS1024:")
114+
var codeTrimmed = text.TrimEnd(':');
115+
var code = codeTrimmed.Trim();
116+
if (code.Length is < 5 or > 7)
74117
{
75-
var hrefPart = element
76-
.GetProperty("href")
77-
.ToString();
78-
79-
var code = element
80-
.GetProperty("toc_title")
81-
.ToString();
82-
83-
var link = hrefPart.StartsWith("../misc/", StringComparison.Ordinal)
84-
? "https://docs.microsoft.com/en-us/dotnet/csharp/" + hrefPart.Replace("../", string.Empty, StringComparison.Ordinal)
85-
: DocumentationLink.AbsoluteUri + "/" + hrefPart;
86-
87-
var rule = await GetRuleByCode(code, link);
88-
if (rule is not null)
89-
{
90-
data.Rules.Add(rule);
91-
}
118+
continue;
92119
}
93-
}
94-
}
95-
}
96120

97-
private static async Task<Rule?> GetRuleByCode(
98-
string code,
99-
string link)
100-
{
101-
var web = new HtmlWeb();
102-
var htmlDoc = await web
103-
.LoadFromWebAsync(link)
104-
.ConfigureAwait(false);
121+
// Get the description from the following text
122+
var description = string.Empty;
123+
var nextSibling = strongNode.NextSibling;
124+
if (nextSibling is not null)
125+
{
126+
var deEntitized = HtmlEntity.DeEntitize(nextSibling.InnerText);
127+
var trimmed = deEntitized.Trim();
128+
var withoutColon = trimmed.TrimStart(':');
129+
description = withoutColon.Trim();
130+
}
105131

106-
if (htmlDoc.DocumentNode.HasTitleWithAccessDenied())
107-
{
108-
return null;
109-
}
132+
var link = categoryUrl + "#" + code.ToLowerInvariant();
110133

111-
var mainNode = htmlDoc.DocumentNode.SelectSingleNode("//main[@id='main']");
112-
if (mainNode is null)
113-
{
114-
return null;
134+
rules.Add(
135+
new Rule(
136+
code,
137+
code,
138+
link,
139+
category: null,
140+
description));
141+
}
115142
}
116-
117-
var header = mainNode.SelectSingleNode(".//h1");
118-
119-
var paragraphs = mainNode
120-
.SelectNodes(".//p")
121-
.ToList();
122-
123-
if (paragraphs.Count < 2)
143+
catch
124144
{
125-
return null;
145+
// Ignore errors for individual category pages
126146
}
127147

128-
var title = header.InnerText;
129-
var description = paragraphs[1].InnerText;
130-
131-
return new Rule(
132-
code,
133-
title,
134-
link,
135-
category: null,
136-
description);
148+
return rules;
137149
}
138150
}

0 commit comments

Comments
 (0)