/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Tools/GenerateRulesMissingDocumentation/Program.cs
193 строки
6 KB
Jan Jones
Avoid flaky "rules missing documentation" check due to rate limiting (#82745)
13 мар 2026, 20:58
Не верифицирован
13 мар 2026, 20:58
822aba7
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Immutable; using System.Globalization; using System.Net; using System.Reflection; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; const int expectedArguments = 4; const string validateOnlyPrefix = "-validateOnly:"; const string rulesMissingDocumentationFileName = "RulesMissingDocumentation.md"; if (args.Length != expectedArguments) { await Console.Error.WriteLineAsync($"Excepted {expectedArguments} arguments, found {args.Length}: {string.Join(';', args)}").ConfigureAwait(false); return 1; } if (!args[0].StartsWith(validateOnlyPrefix, StringComparison.OrdinalIgnoreCase)) { await Console.Error.WriteLineAsync($"Excepted the first argument to start with `{validateOnlyPrefix}`. found `{args[0]}`.").ConfigureAwait(false); return 1; } if (!bool.TryParse(args[0][validateOnlyPrefix.Length..], out var validateOnly)) { validateOnly = false; } var httpClient = new HttpClient(); string analyzerDocumentationFileDir = args[1]; string binDirectory = args[2]; string configuration = args[3]; var directory = Directory.CreateDirectory(analyzerDocumentationFileDir); var fileWithPath = Path.Combine(directory.FullName, rulesMissingDocumentationFileName); var builder = new StringBuilder(); builder.Append(@"# Rules without documentation Rule ID | Missing Help Link | Title | --------|-------------------|-------| "); var actualContent = Array.Empty<string>(); if (validateOnly) { actualContent = File.ReadAllLines(fileWithPath); } var allRulesById = getAllRulesById(binDirectory, configuration); foreach (var ruleById in allRulesById) { string ruleId = ruleById.Key; DiagnosticDescriptor descriptor = ruleById.Value; var helpLinkUri = descriptor.HelpLinkUri; if (!string.IsNullOrWhiteSpace(helpLinkUri) && await checkHelpLinkAsync(helpLinkUri).ConfigureAwait(false)) { // Rule with valid documentation link continue; } // The angle brackets around helpLinkUri are added to follow MD034 rule: // https://github.com/DavidAnson/markdownlint/blob/82cf68023f7dbd2948a65c53fc30482432195de4/doc/Rules.md#md034---bare-url-used if (!string.IsNullOrWhiteSpace(helpLinkUri)) { helpLinkUri = $"<{helpLinkUri}>"; } var escapedTitle = descriptor.Title.ToString(CultureInfo.InvariantCulture).Replace("<", "\\<"); var line = $"{ruleId} | {helpLinkUri} | {escapedTitle} |"; if (validateOnly) { // We consider having "extra" entries as valid. This is to prevent CI failures due to rules being documented. // However, we consider "missing" entries as invalid. This is to force updating the file when new rules are added. if (!actualContent.Contains(line)) { await Console.Error.WriteLineAsync($"Missing entry in '{fileWithPath}'. Please add the below entry to this file to fix the build:").ConfigureAwait(false); await Console.Error.WriteLineAsync(line).ConfigureAwait(false); // The file is missing an entry. Mark it as invalid and break the loop as there is no need to continue validating. return 1; } } else { builder.AppendLine(line); } } if (!validateOnly) { File.WriteAllText(fileWithPath, builder.ToString()); } // NOTE: Network errors (timeouts, rate limits, and 5xx status codes) are not considered failures. async Task<bool> checkHelpLinkAsync(string helpLink) { try { if (!Uri.TryCreate(helpLink, UriKind.Absolute, out var uri)) { return false; } var request = new HttpRequestMessage(HttpMethod.Head, uri); using var response = await httpClient.SendAsync(request).ConfigureAwait(false); var success = response?.StatusCode == HttpStatusCode.OK; if (!success && response is not null) { Console.WriteLine($"##[warning]Failed to check '{helpLink}': {response.StatusCode}"); if ((int)response.StatusCode is (int)HttpStatusCode.TooManyRequests or >= 500) { return true; } } return success; } catch (TaskCanceledException) { Console.WriteLine($"##[warning]Timeout while checking '{helpLink}'."); return true; } catch (HttpRequestException e) { Console.WriteLine($"##[warning]Failed while checking '{helpLink}' (${e.StatusCode}, ${e.HttpRequestError}): ${e.Message}"); return true; } } static SortedList<string, DiagnosticDescriptor> getAllRulesById(string binDirectory, string configuration) { var allRulesById = new SortedList<string, DiagnosticDescriptor>(); foreach (string assembly in s_assemblies) { var assemblyName = Path.GetFileNameWithoutExtension(assembly); string path = Path.Combine(binDirectory, assemblyName, configuration, "netstandard2.0", assembly); if (!File.Exists(path)) { throw new Exception($"'{path}' does not exist"); } var analyzerFileReference = new AnalyzerFileReference(path, AnalyzerAssemblyLoader.Instance); analyzerFileReference.AnalyzerLoadFailed += (sender, e) => throw e.Exception ?? new NotSupportedException(e.Message); var analyzers = analyzerFileReference.GetAnalyzersForAllLanguages(); foreach (var analyzer in analyzers) { foreach (var rule in analyzer.SupportedDiagnostics) { allRulesById[rule.Id] = rule; } } } return allRulesById; } return 0; partial class Program { private static readonly ImmutableArray<string> s_assemblies = ImmutableArray.Create( "Microsoft.CodeAnalysis.Features.dll", "Microsoft.CodeAnalysis.CSharp.Features.dll", "Microsoft.CodeAnalysis.VisualBasic.Features.dll"); private sealed class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader { public static IAnalyzerAssemblyLoader Instance = new AnalyzerAssemblyLoader(); private AnalyzerAssemblyLoader() { } public void AddDependencyLocation(string fullPath) { } public Assembly LoadFromPath(string fullPath) { return Assembly.LoadFrom(fullPath); } } }