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