Skip to content

Close the gaps in the documentation generator's configuration key extractor - #1421

Merged
meziantou merged 2 commits into
mainfrom
feature/doc-gen-config-key-gaps-b7c067
Sep 6, 2026
Merged

meziantou merged 2 commits into
mainfrom
feature/doc-gen-config-key-gaps-b7c067

Conversation

@meziantou

Copy link
Copy Markdown
Owner

What

GetRuleConfigurationKeys in the documentation generator only recognized fields whose type was exactly ConfigurationDefinition<T>, and TryGetRuleIdPrefix gave up unless the key's first two characters were MA. Three consequences:

  • dotnet_diagnostic.MA0048.excluded_symbol_names — the one option written with a dotnet_diagnostic. prefix — was never attributed to MA0048, so ValidateRuleDocumentationContainsConfigurationKeys never checked that MA0048.md mentions it, and the generated "Configurable" tooltip listed 8 keys instead of 9.
  • The definitions held in the MA0220 registry (InvalidRegexConfigurationAnalyzer.RegexConfigurations, an array) were not walked at all.
  • A key that could not be attributed to any rule was silently dropped.

Every one of those keys is in fact documented today, by hand. The documentation check is the only automated guard that a configurable rule documents its options, and it had holes exactly where a rule deviates from the norm — which is when documentation is most likely to be missed.

Changes

Extractor (src/DocumentationGenerator/Program.cs)

  • Arrays and collections of ConfigurationDefinition<T> are walked in addition to bare fields. The type filter still runs before GetValue, so no unrelated static constructors are triggered. A container whose shape the walker cannot traverse now throws rather than dropping its keys quietly.
  • A leading dotnet_diagnostic. is stripped before matching the rule id.
  • A key that cannot be attributed to any rule fails the run, with an allow list for options not owned by a rule (currently just max_line_length).

Option key casing

The keys used three casings (.exclude_tostring_methods, MA0007.IgnoreCatchAllArm, MA0032.allowOverloadsWithOptionalParameters), which worked because Roslyn's key comparer is case-insensitive but made the option surface unguessable. ConfigurationDefinition<T> now holds the current name of an option followed by its legacy names, the first key set in the configuration winning:

private static readonly ConfigurationDefinition<bool> IgnoreCatchAllArmConfiguration =
    new([Rule.Id + ".ignore_catch_all_arm", Rule.Id + ".IgnoreCatchAllArm"], defaultValue: false);
current name legacy name, still supported
MA0007.ignore_catch_all_arm MA0007.IgnoreCatchAllArm
MA0032.allow_overloads_with_optional_parameters MA0032.allowOverloadsWithOptionalParameters
MA0115.report_pascal_case_unmatched_parameter MA0115.ReportPascalCaseUnmatchedParameter

This replaces the IsHidden property, whose only use was hiding the legacy name of an option from the documentation. MA0053 (class_with_virtual_member_shoud_be_sealed) and MA0104 (namepaces_regex) no longer need a second definition and a "read the legacy key, pass it as the default" lookup, and GetConfigurationValue(ISymbol, definition, defaultValue) is removed with its last caller. The generator documents the current name only, while still checking that every name — legacy included — resolves to a rule.

Notes for the reviewer

  • The single-key constructors delegate to this([key], …), so the ~60 one-key definitions are untouched.
  • Keys is an ImmutableArray<string> built from a string[] constructor parameter rather than a collection-expression target: the netstandard2.0 / roslyn4.8 build's System.Collections.Immutable predates CollectionBuilderAttribute (CS9210). This keeps call sites written as collection expressions and the lookup's foreach allocation-free.
  • MA0220 iterates configuration.Keys, so an invalid regex is still reported against the key the user actually wrote.
  • MA0042/MA0045 and the other rules that share a setting across two rule ids keep separate definitions — those are shared options, not aliases, and each is documented under its own rule.
  • The generator produces no markdown change from the Keys refactor itself: the documented key set is identical before and after.

Testing

  • dotnet build clean across all five Roslyn versions.
  • dotnet run --project src/DocumentationGenerator exits 0 and is idempotent. I verified the new guard fires by temporarily emptying the allow list, and that the array walking finds the MA0220 registry's 3 definitions where it previously found none.
  • Full roslyn5.9 suite: 3913 passed, 0 failed. The 349–351 configuration-related tests pass on 4.8 / 4.14 / 5.0 / 5.6 / 5.9; they include the pre-existing tests that set MA0053.class_with_virtual_member_shoud_be_sealed and MA0104.namepaces_regex, so the older aliases are covered through the new mechanism. The three renamed options got their existing tests parameterized over both the new and the legacy key.

…ractor

`GetRuleConfigurationKeys` only recognized fields whose type was exactly
`ConfigurationDefinition<T>`, and `TryGetRuleIdPrefix` gave up unless the key
started with `MA`. As a result `dotnet_diagnostic.MA0048.excluded_symbol_names`
was never attributed to MA0048, so the documentation validation never checked
that MA0048.md mentions it, and the definitions held in the MA0220 registry were
not walked at all. Those keys are documented today, by hand: the only automated
guard that a configurable rule documents its options simply was not running on
them.

The extractor now walks arrays and collections of definitions, strips a leading
`dotnet_diagnostic.` before matching the rule id, and fails the run on a key
that cannot be attributed to any rule, with an allow list for the options that
are not owned by a rule (`max_line_length`). A container with a shape the walker
cannot traverse throws instead of silently dropping its keys.

The option keys also used three casings. `ConfigurationDefinition<T>` now holds
the current name of the option followed by its legacy names, the first key set
in the configuration winning, so the odd ones are renamed to snake_case without
breaking the existing configurations:

- MA0007.IgnoreCatchAllArm            -> MA0007.ignore_catch_all_arm
- MA0032.allowOverloadsWithOptionalParameters
                                      -> MA0032.allow_overloads_with_optional_parameters
- MA0115.ReportPascalCaseUnmatchedParameter
                                      -> MA0115.report_pascal_case_unmatched_parameter

This replaces the `IsHidden` property, whose only use was to hide the legacy
name of an option from the documentation: MA0053 and MA0104 no longer need a
second definition and a second lookup to keep their legacy names working, and
the generator documents the current name while still checking that every name
resolves to a rule.
`main` reworked the same code in the meantime:

- `ConfigurationDefinition<T>` gained `IsRegex`/`RegexOptions`, and MA0220 now
  discovers the regex-valued options by reflection instead of a hardcoded array.
  The definitions carry their key list, so MA0220 iterates `Keys` to keep
  validating the legacy name of an option, and the regex helpers of
  `AnalyzerOptionsExtensions` resolve the definition instead of its current name
  only.
- MA0104 read the legacy option by picking between two definitions. The two are
  merged into one carrying both names, so `GetNamespacesRegex` no longer has to
  choose.
- The documentation generator extracts the definitions once into
  `GetConfigurationDefinitions` and validates that the keys mentioned in the
  documentation are declared. It now collects every key of a definition, so the
  legacy names mentioned in the documentation are declared, while only the
  current name is documented and validated per rule.

The container detection of the generator is restricted to the enumerable types:
`Func<ConfigurationDefinition<string>, string>`, the cached lambda of the MA0220
discovery, is a generic type mentioning a definition but is not a container.

`AllOptionsEndingWithRegexAreValidatedByTheRule` asserted that
`MA0104.namepaces_regex` is one of the keys returned by the MA0220 discovery.
MA0220 still validates it, but as a key of the `namespaces_regex` definition, so
the test considers every key of a definition.
@meziantou
meziantou merged commit c9e3a06 into main Sep 6, 2026
13 checks passed
@meziantou
meziantou deleted the feature/doc-gen-config-key-gaps-b7c067 branch September 6, 2026 06:05
This was referenced Sep 6, 2026
This was referenced Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant