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
8 changes: 4 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
|[MA0004](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0004.md)|Usage|Use Task.ConfigureAwait|<span title='Warning'>⚠️</span>|✔️|✔️|<span title='MA0004.report'>✔️</span>|
|[MA0005](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0005.md)|Performance|Use Array.Empty\<T\>()|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0006](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0006.md)|Usage|Use String.Equals instead of equality operator|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0007](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0007.md)|Style|Add a comma after the last value|<span title='Info'>ℹ️</span>|✔️|✔️|<span title='MA0007.IgnoreCatchAllArm'>✔️</span>|
|[MA0007](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0007.md)|Style|Add a comma after the last value|<span title='Info'>ℹ️</span>|✔️|✔️|<span title='MA0007.ignore_catch_all_arm'>✔️</span>|
|[MA0008](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0008.md)|Performance|Add StructLayoutAttribute|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0009](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0009.md)|Security|Add regex evaluation timeout|<span title='Warning'>⚠️</span>|✔️|❌|❌|
|[MA0010](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0010.md)|Design|Mark attributes with AttributeUsageAttribute|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
Expand All @@ -32,7 +32,7 @@
|[MA0029](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0029.md)|Performance|Combine LINQ methods|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[MA0030](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0030.md)|Performance|Remove useless OrderBy call|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0031](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0031.md)|Performance|Optimize Enumerable.Count() usage|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[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|<span title='Info'>ℹ️</span>|❌|❌|<span title='MA0032.allowOverloadsWithOptionalParameters'>✔️</span>|
|[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|<span title='Info'>ℹ️</span>|❌|❌|<span title='MA0032.allow_overloads_with_optional_parameters'>✔️</span>|
|[MA0033](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0033.md)|Design|Do not tag instance fields with ThreadStaticAttribute|<span title='Warning'>⚠️</span>|✔️|❌|❌|
|[MA0035](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0035.md)|Usage|Do not use dangerous threading methods|<span title='Warning'>⚠️</span>|✔️|❌|❌|
|[MA0036](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0036.md)|Design|Make class static|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
Expand All @@ -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|<span title='Info'>ℹ️</span>|❌|✔️|<span title='MA0045.enable_db_special_cases&#xA;MA0045.enable_sqlite_special_cases'>✔️</span>|
|[MA0046](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0046.md)|Design|Use EventHandler\<T\> to declare events|<span title='Warning'>⚠️</span>|✔️|❌|❌|
|[MA0047](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0047.md)|Design|Declare types in namespaces|<span title='Warning'>⚠️</span>|✔️|❌|❌|
|[MA0048](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0048.md)|Design|File name must match type name|<span title='Warning'>⚠️</span>|✔️|❌|<span title='MA0048.allow_oft_for_all_generic_types&#xA;MA0048.allow_type_name_prefix&#xA;MA0048.exclude_file_local_types&#xA;MA0048.excluded_file_name_parts&#xA;MA0048.excluded_file_name_parts_regex&#xA;MA0048.mode&#xA;MA0048.only_validate_first_type&#xA;MA0048.use_longest_type_name_prefix'>✔️</span>|
|[MA0048](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0048.md)|Design|File name must match type name|<span title='Warning'>⚠️</span>|✔️|❌|<span title='MA0048.allow_oft_for_all_generic_types&#xA;MA0048.allow_type_name_prefix&#xA;MA0048.exclude_file_local_types&#xA;MA0048.excluded_file_name_parts&#xA;MA0048.excluded_file_name_parts_regex&#xA;MA0048.mode&#xA;MA0048.only_validate_first_type&#xA;MA0048.use_longest_type_name_prefix&#xA;dotnet_diagnostic.MA0048.excluded_symbol_names'>✔️</span>|
|[MA0049](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0049.md)|Design|Type name should not match containing namespace|<span title='Error'>❌</span>|✔️|❌|❌|
|[MA0050](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0050.md)|Design|Validate arguments correctly in iterator methods|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[MA0051](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0051.md)|Design|Method is too long|<span title='Warning'>⚠️</span>|✔️|❌|<span title='MA0051.maximum_lines_per_method&#xA;MA0051.maximum_statements_per_method&#xA;MA0051.skip_local_functions'>✔️</span>|
Expand Down Expand Up @@ -114,7 +114,7 @@
|[MA0112](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0112.md)|Performance|Use 'Count \> 0' instead of 'Any()'|<span title='Info'>ℹ️</span>|❌|✔️|❌|
|[MA0113](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0113.md)|Design|Use DateTime.UnixEpoch|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[MA0114](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0114.md)|Design|Use DateTimeOffset.UnixEpoch|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[MA0115](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0115.md)|Usage|Unknown component parameter|<span title='Warning'>⚠️</span>|✔️|❌|<span title='MA0115.ReportPascalCaseUnmatchedParameter'>✔️</span>|
|[MA0115](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0115.md)|Usage|Unknown component parameter|<span title='Warning'>⚠️</span>|✔️|❌|<span title='MA0115.report_pascal_case_unmatched_parameter'>✔️</span>|
|[MA0116](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0116.md)|Design|Parameters with \[SupplyParameterFromQuery\] attributes should also be marked as \[Parameter\]|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0117](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0117.md)|Design|Parameters with \[EditorRequired\] attributes should also be marked as \[Parameter\]|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0118](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0118.md)|Design|\[JSInvokable\] methods must be public|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
Expand Down
8 changes: 4 additions & 4 deletions docs/Rules/MA0007.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -59,7 +59,7 @@ _ = value switch
_ => "other" // Diagnostic reported
};

// With MA0007.IgnoreCatchAllArm = true
// With MA0007.ignore_catch_all_arm = true
_ = value switch
{
1 => "one",
Expand All @@ -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
6 changes: 4 additions & 2 deletions docs/Rules/MA0032.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
6 changes: 4 additions & 2 deletions docs/Rules/MA0040.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
4 changes: 3 additions & 1 deletion docs/Rules/MA0115.md
Original file line number Diff line number Diff line change
Expand Up @@ -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#
Expand Down
110 changes: 89 additions & 21 deletions src/DocumentationGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string>(StringComparer.Ordinal)
{
"max_line_length",
};

var configurationDefinitions = GetConfigurationDefinitions(assemblies);
var ruleConfigurationKeys = GetRuleConfigurationKeys(configurationDefinitions);
var declaredConfigurationKeys = new HashSet<string>(configurationDefinitions.Select(definition => definition.Key), StringComparer.Ordinal);
var declaredConfigurationKeys = new HashSet<string>(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");
Expand Down Expand Up @@ -512,44 +528,88 @@ static string GetBoolean(bool value)
return value ? "✔️" : "❌";
}

static IReadOnlyList<(string Key, bool IsHidden)> GetConfigurationDefinitions(IEnumerable<Assembly> assemblies)
// Each definition is the current name of an option, followed by its legacy names
static IReadOnlyList<IReadOnlyList<string>> GetConfigurationDefinitions(IEnumerable<Assembly> assemblies)
{
var configurationDefinitionType = typeof(ConfigurationDefinition<bool>).GetGenericTypeDefinition();
var keyPropertyName = nameof(ConfigurationDefinition<bool>.Key);
var isHiddenPropertyName = nameof(ConfigurationDefinition<bool>.IsHidden);
var result = new List<(string Key, bool IsHidden)>();
var keysPropertyName = nameof(ConfigurationDefinition<bool>.Keys);
var result = new List<IReadOnlyList<string>>();

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<string> keys)
{
result.Add([.. keys]);
}
}
}
}

return result;
}

static IReadOnlyDictionary<string, IReadOnlyList<string>> GetRuleConfigurationKeys(IEnumerable<(string Key, bool IsHidden)> configurationDefinitions)
static bool IsConfigurationDefinition(Type type)
{
var result = new Dictionary<string, HashSet<string>>(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<object> 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<string, IReadOnlyList<string>> GetRuleConfigurationKeys(IEnumerable<IReadOnlyList<string>> configurationDefinitions)
{
var result = new Dictionary<string, HashSet<string>>(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;

Expand All @@ -574,6 +634,14 @@ static IReadOnlyDictionary<string, IReadOnlyList<string>> GetRuleConfigurationKe
static bool TryGetRuleIdPrefix(string key, [NotNullWhen(true)] out string? ruleId)
{
ruleId = null;

// A few options use the "dotnet_diagnostic.<rule id>.<option>" form instead of the usual "<rule id>.<option>" form
const string DotNetDiagnosticPrefix = "dotnet_diagnostic.";
if (key.StartsWith(DotNetDiagnosticPrefix, StringComparison.Ordinal))
{
key = key.Substring(DotNetDiagnosticPrefix.Length);
}

if (key.Length < 6)
return false;

Expand Down
Loading