diff --git a/docs/README.md b/docs/README.md index 57195cf87..515b5008d 100755 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ |[MA0004](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0004.md)|Usage|Use Task.ConfigureAwait|⚠️|✔️|✔️|✔️| |[MA0005](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0005.md)|Performance|Use Array.Empty\()|⚠️|✔️|✔️|❌| |[MA0006](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0006.md)|Usage|Use String.Equals instead of equality operator|⚠️|✔️|✔️|❌| -|[MA0007](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0007.md)|Style|Add a comma after the last value|ℹ️|✔️|✔️|✔️| +|[MA0007](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0007.md)|Style|Add a comma after the last value|ℹ️|✔️|✔️|✔️| |[MA0008](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0008.md)|Performance|Add StructLayoutAttribute|⚠️|✔️|✔️|❌| |[MA0009](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0009.md)|Security|Add regex evaluation timeout|⚠️|✔️|❌|❌| |[MA0010](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0010.md)|Design|Mark attributes with AttributeUsageAttribute|⚠️|✔️|✔️|❌| @@ -32,7 +32,7 @@ |[MA0029](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0029.md)|Performance|Combine LINQ methods|ℹ️|✔️|✔️|❌| |[MA0030](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0030.md)|Performance|Remove useless OrderBy call|⚠️|✔️|✔️|❌| |[MA0031](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0031.md)|Performance|Optimize Enumerable.Count() usage|ℹ️|✔️|✔️|❌| -|[MA0032](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0032.md)|Usage|Use an overload with a CancellationToken argument, even when no token is available in scope|ℹ️|❌|❌|✔️| +|[MA0032](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0032.md)|Usage|Use an overload with a CancellationToken argument, even when no token is available in scope|ℹ️|❌|❌|✔️| |[MA0033](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0033.md)|Design|Do not tag instance fields with ThreadStaticAttribute|⚠️|✔️|❌|❌| |[MA0035](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0035.md)|Usage|Do not use dangerous threading methods|⚠️|✔️|❌|❌| |[MA0036](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0036.md)|Design|Make class static|ℹ️|✔️|✔️|❌| @@ -47,7 +47,7 @@ |[MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md)|Design|Do not use blocking calls, even when the calling method must become async|ℹ️|❌|✔️|✔️| |[MA0046](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0046.md)|Design|Use EventHandler\ to declare events|⚠️|✔️|❌|❌| |[MA0047](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0047.md)|Design|Declare types in namespaces|⚠️|✔️|❌|❌| -|[MA0048](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0048.md)|Design|File name must match type name|⚠️|✔️|❌|✔️| +|[MA0048](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0048.md)|Design|File name must match type name|⚠️|✔️|❌|✔️| |[MA0049](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0049.md)|Design|Type name should not match containing namespace|❌|✔️|❌|❌| |[MA0050](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0050.md)|Design|Validate arguments correctly in iterator methods|ℹ️|✔️|✔️|❌| |[MA0051](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0051.md)|Design|Method is too long|⚠️|✔️|❌|✔️| @@ -114,7 +114,7 @@ |[MA0112](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0112.md)|Performance|Use 'Count \> 0' instead of 'Any()'|ℹ️|❌|✔️|❌| |[MA0113](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0113.md)|Design|Use DateTime.UnixEpoch|ℹ️|✔️|✔️|❌| |[MA0114](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0114.md)|Design|Use DateTimeOffset.UnixEpoch|ℹ️|✔️|✔️|❌| -|[MA0115](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0115.md)|Usage|Unknown component parameter|⚠️|✔️|❌|✔️| +|[MA0115](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0115.md)|Usage|Unknown component parameter|⚠️|✔️|❌|✔️| |[MA0116](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0116.md)|Design|Parameters with \[SupplyParameterFromQuery\] attributes should also be marked as \[Parameter\]|⚠️|✔️|✔️|❌| |[MA0117](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0117.md)|Design|Parameters with \[EditorRequired\] attributes should also be marked as \[Parameter\]|⚠️|✔️|✔️|❌| |[MA0118](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0118.md)|Design|\[JSInvokable\] methods must be public|⚠️|✔️|✔️|❌| diff --git a/docs/Rules/MA0007.md b/docs/Rules/MA0007.md index 6d40afd8f..791e688e6 100644 --- a/docs/Rules/MA0007.md +++ b/docs/Rules/MA0007.md @@ -47,9 +47,9 @@ A trailing comma is required when `]` is on a following line. ## Configuration -### `MA0007.IgnoreCatchAllArm` +### `MA0007.ignore_catch_all_arm` -By default, the analyzer reports a diagnostic when a catch-all arm (`_`) in a switch expression is missing a trailing comma. You can disable this behavior by setting `MA0007.IgnoreCatchAllArm` to `true`. +By default, the analyzer reports a diagnostic when a catch-all arm (`_`) in a switch expression is missing a trailing comma. You can disable this behavior by setting `MA0007.ignore_catch_all_arm` to `true`. The previous name of this option, `MA0007.IgnoreCatchAllArm`, is still supported. ````csharp // Default behavior (non-compliant) @@ -59,7 +59,7 @@ _ = value switch _ => "other" // Diagnostic reported }; -// With MA0007.IgnoreCatchAllArm = true +// With MA0007.ignore_catch_all_arm = true _ = value switch { 1 => "one", @@ -78,4 +78,4 @@ _ = value switch ```ini # Ignore missing trailing comma on catch-all arms in switch expressions -MA0007.IgnoreCatchAllArm = true +MA0007.ignore_catch_all_arm = true diff --git a/docs/Rules/MA0032.md b/docs/Rules/MA0032.md index 0341e4275..bddbcb5b9 100644 --- a/docs/Rules/MA0032.md +++ b/docs/Rules/MA0032.md @@ -22,11 +22,13 @@ class Test ## Configuration ```` -MA0032.allowOverloadsWithOptionalParameters = false +MA0032.allow_overloads_with_optional_parameters = false ```` +The previous name of this option, `MA0032.allowOverloadsWithOptionalParameters`, is still supported. + ````c# -Foo.Bar(); // report when MA0032.allowOverloadsWithOptionalParameters is true +Foo.Bar(); // report when MA0032.allow_overloads_with_optional_parameters is true class Foo { diff --git a/docs/Rules/MA0040.md b/docs/Rules/MA0040.md index 256f71f4b..97e162a21 100644 --- a/docs/Rules/MA0040.md +++ b/docs/Rules/MA0040.md @@ -24,11 +24,13 @@ This rule only reports a diagnostic when a `CancellationToken` is available in t ## Configuration ```` -MA0032.allowOverloadsWithOptionalParameters = false +MA0032.allow_overloads_with_optional_parameters = false ```` +The previous name of this option, `MA0032.allowOverloadsWithOptionalParameters`, is still supported. + ````c# -Foo.Bar(); // report when MA0032.allowOverloadsWithOptionalParameters is true +Foo.Bar(); // report when MA0032.allow_overloads_with_optional_parameters is true class Foo { diff --git a/docs/Rules/MA0115.md b/docs/Rules/MA0115.md index 1f48ae268..10fcd8ab1 100644 --- a/docs/Rules/MA0115.md +++ b/docs/Rules/MA0115.md @@ -24,9 +24,11 @@ Detects invalid parameter usage in Razor components. When a component allows unmatched parameters, parameters with PascalCase names can still be detected. ```.editorconfig -MA0115.ReportPascalCaseUnmatchedParameter +MA0115.report_pascal_case_unmatched_parameter ``` +The previous name of this option, `MA0115.ReportPascalCaseUnmatchedParameter`, is still supported. + In the following example, `InvalidParameter` is reported as an unmatched parameter. ````c# diff --git a/src/DocumentationGenerator/Program.cs b/src/DocumentationGenerator/Program.cs index 8a7da8784..23c47ef78 100644 --- a/src/DocumentationGenerator/Program.cs +++ b/src/DocumentationGenerator/Program.cs @@ -2,6 +2,7 @@ #pragma warning disable CA1849 #pragma warning disable MA0004 #pragma warning disable MA0009 +using System.Collections; using System.Reflection; using System.Text.Encodings.Web; using System.Text.RegularExpressions; @@ -42,9 +43,24 @@ .Select(type => (DiagnosticSuppressor)Activator.CreateInstance(type)!) .ToList(); +// Options that are not owned by a rule, such as the well-known .editorconfig options shared with other tools +var globalConfigurationKeys = new HashSet(StringComparer.Ordinal) +{ + "max_line_length", +}; + var configurationDefinitions = GetConfigurationDefinitions(assemblies); var ruleConfigurationKeys = GetRuleConfigurationKeys(configurationDefinitions); -var declaredConfigurationKeys = new HashSet(configurationDefinitions.Select(definition => definition.Key), StringComparer.Ordinal); +var declaredConfigurationKeys = new HashSet(configurationDefinitions.SelectMany(definition => definition), StringComparer.Ordinal); + +foreach (var configurationKey in declaredConfigurationKeys.Order(StringComparer.Ordinal)) +{ + if (globalConfigurationKeys.Contains(configurationKey) || TryGetRuleIdPrefix(configurationKey, out _)) + continue; + + documentationValidationErrorCount++; + Console.Error.WriteLine($"Cannot find the rule owning the configuration key '{configurationKey}'. Prefix the key with the rule id, or add it to the global configuration keys of the documentation generator."); +} var sb = new StringBuilder(); sb.Append("# ").Append(assemblies[0].GetName().Name).Append("'s rules\n"); @@ -512,44 +528,88 @@ static string GetBoolean(bool value) return value ? "✔️" : "❌"; } -static IReadOnlyList<(string Key, bool IsHidden)> GetConfigurationDefinitions(IEnumerable assemblies) +// Each definition is the current name of an option, followed by its legacy names +static IReadOnlyList> GetConfigurationDefinitions(IEnumerable assemblies) { - var configurationDefinitionType = typeof(ConfigurationDefinition).GetGenericTypeDefinition(); - var keyPropertyName = nameof(ConfigurationDefinition.Key); - var isHiddenPropertyName = nameof(ConfigurationDefinition.IsHidden); - var result = new List<(string Key, bool IsHidden)>(); + var keysPropertyName = nameof(ConfigurationDefinition.Keys); + var result = new List>(); 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) + if (!CanContainConfigurationDefinitions(field.FieldType)) continue; - var fieldValue = field.GetValue(null); - if (fieldValue is null) - continue; - - if (field.FieldType.GetProperty(keyPropertyName)?.GetValue(fieldValue) is not string key) - continue; - - var isHidden = field.FieldType.GetProperty(isHiddenPropertyName)?.GetValue(fieldValue) is bool value && value; - result.Add((key, isHidden)); + foreach (var configuration in EnumerateConfigurationDefinitions(field.GetValue(null))) + { + if (configuration.GetType().GetProperty(keysPropertyName)?.GetValue(configuration) is IEnumerable keys) + { + result.Add([.. keys]); + } + } } } return result; } -static IReadOnlyDictionary> GetRuleConfigurationKeys(IEnumerable<(string Key, bool IsHidden)> configurationDefinitions) +static bool IsConfigurationDefinition(Type type) { - var result = new Dictionary>(StringComparer.Ordinal); + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ConfigurationDefinition<>); +} - foreach (var (key, isHidden) in configurationDefinitions) +// A rule does not always expose its options as fields of their own: some of them keep a registry of the options +// they handle. Only the fields that can hold a definition are read, so that unrelated static constructors are not run. +static bool CanContainConfigurationDefinitions(Type type) +{ + if (IsConfigurationDefinition(type)) + return true; + + if (type.IsArray) + return type.GetElementType() is { } elementType && CanContainConfigurationDefinitions(elementType); + + // Only the collections of definitions are walked: another generic type that merely mentions the type of a + // definition, such as a cached lambda taking one, is not a container + if (type.IsGenericType && typeof(IEnumerable).IsAssignableFrom(type)) + return Array.Exists(type.GetGenericArguments(), CanContainConfigurationDefinitions); + + return false; +} + +static IEnumerable EnumerateConfigurationDefinitions(object? value) +{ + if (value is null) + yield break; + + if (IsConfigurationDefinition(value.GetType())) { - if (isHidden) - continue; + yield return value; + yield break; + } + + // Only enumerable containers can be walked. Throw instead of silently dropping the definitions of a container + // with an unsupported shape, as the documentation of the rules owning them would not be validated anymore. + if (value is not IEnumerable enumerable) + throw new InvalidOperationException($"Cannot enumerate the configuration definitions of '{value.GetType()}'"); + + foreach (var item in enumerable) + { + foreach (var configuration in EnumerateConfigurationDefinitions(item)) + { + yield return configuration; + } + } +} + +static IReadOnlyDictionary> GetRuleConfigurationKeys(IEnumerable> configurationDefinitions) +{ + var result = new Dictionary>(StringComparer.Ordinal); + foreach (var definition in configurationDefinitions) + { + // Only the current name of an option is documented: its legacy names are still supported, but they must not be advertised + var key = definition[0]; if (TryGetRuleIdPrefix(key, out var ruleId) is false) continue; @@ -574,6 +634,14 @@ static IReadOnlyDictionary> GetRuleConfigurationKe static bool TryGetRuleIdPrefix(string key, [NotNullWhen(true)] out string? ruleId) { ruleId = null; + + // A few options use the "dotnet_diagnostic..