Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
424 changes: 212 additions & 212 deletions README.md

Large diffs are not rendered by default.

424 changes: 212 additions & 212 deletions docs/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/Rules/MA0045.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ It also shares the same Db\* await-using special-cases as MA0042 by default for:
````
# .editorconfig file
# Enable SQLite special-cases for MA0042/MA0045. default: true
MA0042.enable_sqlite_special_cases = true
MA0045.enable_sqlite_special_cases = true

# Assume DbConnection/DbCommand/DbDataReader/DbTransaction/DbBatch hierarchies are sealed
# for await-using analysis. default: true
MA0042.enable_db_special_cases = true
MA0045.enable_db_special_cases = true
````

You can also exclude specific method/property diagnostics for MA0042/MA0045 using assembly-level annotations.
Expand Down
9 changes: 9 additions & 0 deletions docs/Rules/MA0124.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ Source: [LoggerParameterTypeAnalyzer.cs](https://github.com/meziantou/Meziantou.

This rule ensures the parameters for Microsoft.Extensions.Logging's logger are of the expected types. You can configure the expected types using a configuration file.

## Configuration

You can configure whether non-constant format strings are allowed:

````
# .editorconfig file
MA0124.allow_non_constant_formats = true
````

To configure the rule, you need to create a file named `LoggerParameterTypes.txt` or `LoggerParameterTypes.*.txt`.
- Each line is of the form `PropertyName;ExpectedType1;ExpectedType2;ExpectedType3;...`
- A type is represented by its CLR metadata name or [XML Comment ID reference](https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/documentation-comments.md)
Expand Down
9 changes: 9 additions & 0 deletions docs/Rules/MA0139.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ Source: [LoggerParameterTypeAnalyzer.cs](https://github.com/meziantou/Meziantou.

This rule ensures the parameters for Serilog's logger are of the expected types. You can configure the expected types using a configuration file.

## Configuration

You can configure whether non-constant format strings are allowed:

````
# .editorconfig file
MA0139.allow_non_constant_formats = true
````

To configure the rule, you need to create a file named `LoggerParameterTypes.txt` or `LoggerParameterTypes.*.txt`.
- Each line is of the form `PropertyName;ExpectedType1;ExpectedType2;ExpectedType3;...`
- A type is represented by its CLR metadata name or [XML Comment ID reference](https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/documentation-comments.md)
Expand Down
127 changes: 120 additions & 7 deletions src/DocumentationGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
#pragma warning disable CA1849
#pragma warning disable MA0004
#pragma warning disable MA0009
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using Meziantou.Analyzer.Configurations;
using Meziantou.Framework;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
Expand All @@ -16,6 +19,7 @@
return 1;
}
var fileWritten = 0;
var documentationValidationErrorCount = 0;

var assemblies = new[] { typeof(Meziantou.Analyzer.Rules.CommaAnalyzer).Assembly, typeof(Meziantou.Analyzer.Rules.CommaFixer).Assembly };
var diagnosticAnalyzers = assemblies.SelectMany(assembly => assembly.GetExportedTypes())
Expand All @@ -38,9 +42,11 @@
.Select(type => (DiagnosticSuppressor)Activator.CreateInstance(type)!)
.ToList();

var ruleConfigurationKeys = GetRuleConfigurationKeys(assemblies);

var sb = new StringBuilder();
sb.Append("# ").Append(assemblies[0].GetName().Name).Append("'s rules\n");
var rulesTable = GenerateRulesTable(diagnosticAnalyzers, codeFixProviders);
var rulesTable = GenerateRulesTable(diagnosticAnalyzers, codeFixProviders, ruleConfigurationKeys);
sb.Append(rulesTable);

var suppressorsTable = GenerateSuppressorsTable(diagnosticSuppressors);
Expand All @@ -59,7 +65,7 @@
// nuget.org's markdown support is limited. Raw html in table is not supported.
var readmePath = outputFolder / "README.md";
var readmeContent = await File.ReadAllTextAsync(readmePath);
var newContent = Regex.Replace(readmeContent, "(?<=<!-- rules -->\\r?\\n).*(?=<!-- rules -->)", "\n" + GenerateRulesTable(diagnosticAnalyzers, codeFixProviders, addTitle: false) + "\n", RegexOptions.Singleline);
var newContent = Regex.Replace(readmeContent, "(?<=<!-- rules -->\\r?\\n).*(?=<!-- rules -->)", "\n" + GenerateRulesTable(diagnosticAnalyzers, codeFixProviders, ruleConfigurationKeys, addTitle: false) + "\n", RegexOptions.Singleline);
newContent = Regex.Replace(newContent, "(?<=<!-- suppressions -->\\r?\\n).*(?=<!-- suppressions -->)", "\n" + GenerateSuppressorsTable(diagnosticSuppressors) + "\n", RegexOptions.Singleline);
newContent = Regex.Replace(newContent, "(?<=<!-- refactorings -->\\r?\\n).*(?=<!-- refactorings -->)", "\n" + GenerateRefactoringsTable(codeRefactoringProviders) + "\n", RegexOptions.Singleline);
WriteFileIfChanged(readmePath, newContent);
Expand All @@ -74,6 +80,21 @@

// Update title in rule pages and add links to source code
{
void ValidateRuleDocumentationContainsConfigurationKeys(FullPath path, string ruleId, string content)
{
if (!ruleConfigurationKeys.TryGetValue(ruleId, out var configurationKeys))
return;

foreach (var configurationKey in configurationKeys)
{
if (content.Contains(configurationKey, StringComparison.Ordinal))
continue;

documentationValidationErrorCount++;
Console.Error.WriteLine($"Missing configuration key '{configurationKey}' in {path.MakePathRelativeTo(outputFolder)}");
}
}

var rules = new HashSet<string>(StringComparer.Ordinal);
foreach (var diagnosticAnalyzer in diagnosticAnalyzers)
{
Expand Down Expand Up @@ -151,11 +172,13 @@ void AddLink(string name)
sourceLinks.Sort(StringComparer.Ordinal);
newContent = Regex.Replace(newContent, "(?<=<!-- sources -->\\r?\\n).*(?=<!-- sources -->)", (sourceLinks.Count == 1 ? "Source: " : "Sources: ") + string.Join(", ", sourceLinks) + "\n", RegexOptions.Singleline);

ValidateRuleDocumentationContainsConfigurationKeys(detailPath, diagnostic.Id, newContent);
WriteFileIfChanged(detailPath, newContent);
}
else
{
WriteFileIfChanged(detailPath, title);
ValidateRuleDocumentationContainsConfigurationKeys(detailPath, diagnostic.Id, title);
}
}
}
Expand Down Expand Up @@ -203,6 +226,13 @@ void GenerateFile(FullPath outputPath, Action<StringBuilder> code)
process.BeginErrorReadLine();
await process.WaitForExitAsync();
}

if (documentationValidationErrorCount > 0)
{
Console.Error.WriteLine($"{documentationValidationErrorCount} documentation validation error(s) found.");
return 1;
}

return fileWritten;

void WriteFileIfChanged(FullPath path, string content)
Expand All @@ -227,11 +257,11 @@ void WriteFileIfChanged(FullPath path, string content)
}
}

static string GenerateRulesTable(List<DiagnosticAnalyzer> diagnosticAnalyzers, List<CodeFixProvider> codeFixProviders, bool addTitle = true)
static string GenerateRulesTable(List<DiagnosticAnalyzer> diagnosticAnalyzers, List<CodeFixProvider> codeFixProviders, IReadOnlyDictionary<string, IReadOnlyList<string>> ruleConfigurationKeys, bool addTitle = true)
{
var sb = new StringBuilder();
sb.Append("|Id|Category|Description|Severity|Is enabled|Code fix|\n");
sb.Append("|--|--------|-----------|:------:|:--------:|:------:|\n");
sb.Append("|Id|Category|Description|Severity|Is enabled|Code fix|Configurable|\n");
sb.Append("|--|--------|-----------|:------:|:--------:|:------:|:----------:|\n");

foreach (var diagnostic in diagnosticAnalyzers.SelectMany(diagnosticAnalyzer => diagnosticAnalyzer.SupportedDiagnostics).DistinctBy(diag => diag.Id).OrderBy(diag => diag.Id, StringComparer.Ordinal))
{
Expand Down Expand Up @@ -263,12 +293,30 @@ static string GenerateRulesTable(List<DiagnosticAnalyzer> diagnosticAnalyzers, L
sb.Append(GetSeverity(diagnostic.DefaultSeverity));
}

ruleConfigurationKeys.TryGetValue(diagnostic.Id, out var configurationKeys);
configurationKeys ??= [];

sb.Append('|')
.Append(GetBoolean(diagnostic.IsEnabledByDefault))
.Append('|')
.Append(GetBoolean(hasCodeFix))
.Append('|')
.Append('\n');
.Append('|');

if (configurationKeys.Count > 0 && addTitle)
{
sb.Append("<span title='")
.Append(HtmlEncoder.Default.Encode(string.Join("\n", configurationKeys)))
.Append("'>")
.Append(GetBoolean(true))
.Append("</span>");
}
else
{
sb.Append(GetBoolean(configurationKeys.Count > 0));
}

sb.Append('|')
.Append('\n');
}

return sb.ToString();
Expand Down Expand Up @@ -410,3 +458,68 @@ static string GetBoolean(bool value)
{
return value ? "✔️" : "❌";
}

static IReadOnlyDictionary<string, IReadOnlyList<string>> GetRuleConfigurationKeys(IEnumerable<Assembly> assemblies)
{
var configurationDefinitionType = typeof(ConfigurationDefinition<bool>).GetGenericTypeDefinition();
var keyPropertyName = nameof(ConfigurationDefinition<bool>.Key);
var isHiddenPropertyName = nameof(ConfigurationDefinition<bool>.IsHidden);
var result = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);

foreach (var type in assemblies.SelectMany(assembly => assembly.GetTypes()))
{
foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
{
if (!field.FieldType.IsGenericType || field.FieldType.GetGenericTypeDefinition() != configurationDefinitionType)
continue;

var fieldValue = field.GetValue(null);
if (fieldValue is null)
continue;

if (field.FieldType.GetProperty(isHiddenPropertyName)?.GetValue(fieldValue) is bool isHidden && isHidden)
continue;

if (field.FieldType.GetProperty(keyPropertyName)?.GetValue(fieldValue) is not string key)
continue;

if (TryGetRuleIdPrefix(key, out var ruleId) is false)
continue;

if (!result.TryGetValue(ruleId, out var keys))
{
keys = [];
result.Add(ruleId, keys);
}

keys.Add(key);
}
}

var output = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
foreach (var item in result)
{
output[item.Key] = [.. item.Value.Order(StringComparer.Ordinal)];
}

return output;
}

static bool TryGetRuleIdPrefix(string key, [NotNullWhen(true)] out string? ruleId)
{
ruleId = null;
if (key.Length < 6)
return false;

if (key[0] != 'M' || key[1] != 'A')
return false;

if (!char.IsAsciiDigit(key[2]) || !char.IsAsciiDigit(key[3]) || !char.IsAsciiDigit(key[4]) || !char.IsAsciiDigit(key[5]))
return false;

if (key.Length > 6 && key[6] != '.')
return false;

ruleId = key.Substring(0, 6);
return true;
}
Loading