Unify analyzer diagnostic IDs and documentation - #3437
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee30df8560
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Review: MP#### analyzer renumbering (#3437)
Solid, well-executed change overall — renumbering is applied consistently across both analyzer projects, the AnalyzerReleases.*.md rename bookkeeping follows the existing convention correctly (old shipped IDs moved to "Removed Rules" with a "Renamed to" note, matching how ConflictingDependsOnAttribute → MPDEP002 was already handled on main), and generating the docs pages from the release-tracking files instead of hand-writing 19 markdown pages is a good call — it makes the release files the actual source of truth instead of just release-tracking metadata.
Two things worth addressing, both because they touch the exact problem this PR is trying to solve (one clean, collision-free MP#### ID space):
1. The doc generator can silently merge a duplicate ID instead of failing
docs/scripts/generate-analyzer-docs.mjs builds one Map of active rules by folding all four AnalyzerReleases.*.md files together (applyReleaseMetadata, called once per file). If the same MP#### ID ever appeared as a "New Rule" in two different files — e.g. a future PR adds a rule to ModularPipelines.Development.Analyzers and picks an ID that collides with one already used in ModularPipelines.Analyzers — the second file processed just overwrites the first in the map (activeRulesById.set(id, rule)), and the generator happily produces one doc page for whichever entry won. No error, no warning.
This matters here specifically because the whole point of the PR is a single, unshared MP0001-MP0019 numbering family spread across two independently-built projects that can't reference each other's DiagnosticId constants at compile time. The two AnalyzerMetadataTests added in this PR each only check their own project's IDs in isolation (MP0001-10,13-19 in one test, MP0011-12 in the other) — neither test, nor anything else in CI, actually verifies the two sets don't overlap. The doc generator is the one place that sees the full merged set, so it's the natural place to assert uniqueness (e.g. throw if activeRulesById.has(id) is already true for a "New Rules" entry) rather than silently resolving collisions by file-processing order.
2. DocumentationBaseUrl is duplicated instead of reusing the existing factory pattern
The literal "https://thomhurst.github.io/ModularPipelines/docs/analyzers/" is now declared independently in six places: DiagnosticDescriptorFactory.cs, VirtualCommandAnalyzer.cs, VirtualSwitchPropertyAnalyzer.cs, and three test files. Two of those (VirtualCommandAnalyzer.cs and VirtualSwitchPropertyAnalyzer.cs) live in the same ModularPipelines.Development.Analyzers assembly and duplicate not just the constant but the whole descriptor-construction boilerplate (new DiagnosticDescriptor(id, title, format, category, severity, ..., helpLinkUri: DocumentationBaseUrl + id)).
The core ModularPipelines.Analyzers project already solved this exact problem with DiagnosticDescriptorFactory.Create(...), which centralizes the base URL and the help-link construction in one place. ModularPipelines.Development.Analyzers doesn't reuse that pattern (it can't reference the other assembly — analyzers are typically kept isolated to avoid version conflicts when both load into the same Roslyn host), so it re-implements the same thing by hand, twice, with a copy-pasted constant. A small internal factory local to ModularPipelines.Development.Analyzers (mirroring DiagnosticDescriptorFactory) would remove the duplication within that project immediately, and is the kind of thing that's normally shared between sibling analyzer packages via a linked source file (<Compile Include="../Shared/..\.cs" />) rather than a project reference, precisely because of that isolation constraint. Right now, if the docs domain ever moves, someone has to remember to update six independent string literals instead of one or two.
Minor
AnalyzerMetadataTests.PublicRulesUseUnifiedIdsAndHelpLinksasserts exact array order againstEnumerable.Range(1, 10).Concat(Enumerable.Range(13, 7)). It works today, but every future rule addition/removal requires manually keeping the analyzer array's literal order in sync with a hand-maintained numeric range — an order-agnostic check (sort both sides, or assert set membership + "no gaps") would be less fragile to maintain than keeping two representations of the same sequence in lockstep.docs/package.json'sprebuild/prestarthooks shell out tonpm run ...even though the docs site is otherwise yarn-managed (CI runsyarn build, which does trigger thepre/postnpm-style hooks under yarn classic, so it works) — usingyarnthere instead would keep the project on one package-manager invocation instead of implicitly depending on npm being present alongside yarn.
None of these block the PR — the collision-detection gap is the one I'd actually want fixed before this ships, since it's cheap to add and it's exactly the invariant this PR exists to establish.
ee30df8 to
dc962e8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc962e8df4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Code review
Good follow-through since the last review: the doc generator now throws on a duplicate MP#### ID instead of silently letting file-processing order win (generate-analyzer-docs.mjs), and the DocumentationBaseUrl/descriptor-construction duplication across VirtualCommandAnalyzer.cs and VirtualSwitchPropertyAnalyzer.cs has been eliminated by adding a DiagnosticDescriptorFactory to ModularPipelines.Development.Analyzers mirroring the core project's factory. The docs/package.json hooks were also switched from the previously-flagged npm run to yarn, and the analyzer ID array assertion already uses CollectionAssert.AreEquivalent/IsEquivalentTo, which is order-insensitive, so that minor concern doesn't need further changes.
Three new items surfaced in this pass, one of which is a real functional bug and two of which are documentation-completeness/architecture gaps worth addressing before this ships, since (like last time) they all touch the exact invariant this PR exists to establish — a single reliable MP#### ID space with working docs.
1. HelpLinkUri will 404 for every rule because the docs site is versioned
Both DiagnosticDescriptorFactory implementations bake in https://thomhurst.github.io/ModularPipelines/docs/analyzers/ + id as the permanent, shipped help link. But docs/versions.json contains ["3.x"] and docs/versioned_docs/version-3.x/ exists with no analyzers/ directory and no lastVersion override in docusaurus.config.ts — so with Docusaurus's default versioning behavior, the bare /docs/... path serves the last released version (3.x), and everything under docs/docs/ (where these 20 new pages live) is only reachable at /docs/next/... until the next version cut. Every help link an IDE surfaces today will 404.
This is architecturally the same class of problem the PR already fixed for ID collisions: the fix works in isolation (docs generator + tests are internally consistent) but doesn't account for how the surrounding system (versioned docs deploy) actually serves the content. Two reasonable fixes: point the base URL at .../docs/next/analyzers/ until 3.x is next cut, or add the pages to docs/versioned_docs/version-3.x/analyzers/ as well so the bare path works immediately. A redirect/alias at the Docusaurus level would also work and avoid ever having to remember this again.
2. The ID migration table is missing 5 of the renames this same PR performs
docs/docs/how-to/analyzers.md (the migration table) documents legacy→current ID mappings, but is missing:
AwaitThis→MP0007StatefulModule→MP0008MPDEP001→MP0009MPDEP002→MP0005(in addition to the existing, still-correctConflictingDependsOnAttribute→MP0005row, sinceMPDEP002was a separate, previously-unshipped ID for the same rule)MPDEP003→MP0010
All five were live/unshipped IDs on main before this PR per AnalyzerReleases.Unshipped.md. Anyone with a #pragma warning disable MPDEP001 (etc.) or a matching .editorconfig entry gets no migration guidance for exactly the IDs this PR renumbers.
Since the table is now hand-maintained separately from the generator, consider generating this table from the same AnalyzerReleases.*.md "Removed Rules" Renamed to bookkeeping the generator already reads — that would make the same class of omission structurally impossible instead of relying on someone remembering to update prose by hand each time a rule is renumbered.
3. generate-analyzer-docs.mjs mis-parses a ### Changed Rules section (currently dormant)
The heading regex /^###\s+(New|Removed|Changed) Rules$/i opts into parsing Changed Rules sections, and rows there do get turned into a rule via const [id, category, severity, ...notes] = columns;. But Roslyn's standard Changed Rules table is 6 columns (Rule ID | New Category | New Severity | Old Category | Old Severity | Notes), not the 4-column New/Removed shape. The old category/severity/notes all collapse into description, and since renderIndex interpolates that description unescaped into a markdown table cell, the embedded | characters will corrupt the generated index.md table the first time any rule's category or severity changes (a normal, expected maintenance operation for AnalyzerReleases.md files).
None of the four release files currently have a Changed Rules section, so nothing is broken today — but the code path is live, not speculative, so it'll fail silently (wrong output, not a thrown error) the first time it's exercised. Worth branching the column destructuring on section === 'changed' now while the fix is cheap and the failure mode is fresh in mind, rather than leaving a landmine for whoever changes a rule's severity later.
None of these are inherent to the renumbering approach itself — the core design (one shared MP#### space, generated docs as source of truth, a factory to remove boilerplate) is sound and the right call for this problem.
5dc7a0a to
c2f8665
Compare
dc962e8 to
7d33f63
Compare
There was a problem hiding this comment.
Code review
Checked this pass against my previous review comments on this PR. Confirmed fixed:
- Duplicate
MP####ID collision now throws ingenerate-analyzer-docs.mjs(applyReleaseMetadata) instead of silently letting file-processing order win. DocumentationBaseUrl/descriptor-construction duplication inModularPipelines.Development.Analyzersis gone now that it has its ownDiagnosticDescriptorFactory, mirroring the core project's.docs/package.json'sprebuild/prestarthooks now callyarn ...instead ofnpm run ....- The analyzer ID array assertions in both
AnalyzerMetadataTestsare order-insensitive (CollectionAssert.AreEquivalent/IsEquivalentTo).
Still open from the last review — none block merging, but all three touch the exact invariant this PR exists to establish (one working MP#### ID space with correct docs), so I'd still like them addressed before this ships:
1. HelpLinkUri will 404 for every rule (docs are versioned)
Both DiagnosticDescriptorFactory implementations (src/ModularPipelines.Analyzers/.../DiagnosticDescriptorFactory.cs:11 and src/ModularPipelines.Development.Analyzers/DiagnosticDescriptorFactory.cs:7) still hardcode https://thomhurst.github.io/ModularPipelines/docs/analyzers/ + id. docs/versions.json still only contains ["3.x"], and docs/versioned_docs/version-3.x/ has no analyzers/ directory. With Docusaurus's default versioning, the bare /docs/... path serves the last released version (3.x), so every new page under docs/docs/analyzers/ is only reachable at /docs/next/analyzers/... until the next version cut — every help link an IDE surfaces today 404s. Either point the base URL at .../docs/next/analyzers/ for now, add the pages under docs/versioned_docs/version-3.x/analyzers/ too, or add a Docusaurus-level redirect.
2. The ID migration table is still missing 5 renames this PR performs
docs/docs/how-to/analyzers.md's migration table still doesn't list:
AwaitThis→MP0007StatefulModule→MP0008MPDEP001→MP0009MPDEP002→MP0005(in addition to the existingConflictingDependsOnAttribute→MP0005row —MPDEP002was a separate, previously-unshipped ID for the same rule perAnalyzerReleases.Unshipped.mdon the base branch)MPDEP003→MP0010
All five were live/unshipped IDs on the base branch before this PR. Anyone with #pragma warning disable MPDEP001 or a matching .editorconfig entry gets no migration guidance for exactly the IDs this PR renumbers. Since this table is hand-maintained separately from the generator, consider generating it from the same "Removed Rules" Renamed to bookkeeping the generator already parses, so this class of omission becomes structurally impossible rather than relying on someone remembering to update prose by hand.
3. generate-analyzer-docs.mjs still mis-parses a ### Changed Rules section (dormant, not exercised today)
The heading regex still matches Changed|New|Removed, and Changed Rules rows still get destructured as [id, category, severity, ...notes] — the same 4-column shape as New/Removed. Roslyn's standard Changed Rules table is 6 columns (Rule ID | New Category | New Severity | Old Category | Old Severity | Notes), so the old category/severity/notes columns would collapse into description, and since renderIndex interpolates that unescaped into a markdown table cell, embedded | characters would corrupt the generated index.md table the first time any rule's category or severity changes. None of the four release files have a Changed Rules section today, so nothing is broken yet, but it's a live code path, not speculative — worth branching the column destructuring on section === 'changed' now while it's cheap, rather than leaving a landmine for the first future severity/category change.
None of these are inherent to the renumbering approach — the core design (one shared MP#### space, generated docs as source of truth, a factory to remove descriptor boilerplate) remains sound and the right call for this problem.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d33f637d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Addressed all current review feedback in
Validation: analyzer tests 68/68, development analyzer tests 30/30, analyzer solution Release build (0 errors), docs generator check (19 pages), focused format clean. @codex review |
Code reviewChecked
One small, non-blocking thing I noticed in this same commit: Assert.AreSequenceEqual(
Enumerable.Range(1, 10).Concat(Enumerable.Range(13, 7)).Select(index => $"MP{index:0000}"),
rules.Select(rule => rule.Id),
SequenceOrder.InAnyOrder);Not blocking — the core design (one shared |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration
* feat(analyzers): catch module authoring errors * refactor(analyzers): split authoring checks * fix: harden module authoring analyzers Scope registration diagnostics to applications, model runtime registration behavior, tighten async analysis, and restore typed resource keys with regression coverage. * Unify analyzer diagnostic IDs and documentation (#3437) * feat(analyzers): unify diagnostic IDs * fix: finish analyzer ID migration * fix(analyzers): harden authoring analysis * fix(analyzers): honor assembly scans * refactor(analyzers): flatten await traversal * fix(analyzers): trace assigned local values * fix(analyzers): cover more module call paths * fix(analyzers): trace nested async work * refactor(analyzers): simplify async traversal * fix(analyzers): handle dynamic module arrays * fix(analyzers): avoid branch-blind tracing * refactor(analyzers): split registration tracing * fix(analyzers): honor execution reachability * fix(analyzers): inspect scan and task edges * fix(analyzers): retain required dependencies * fix(analyzers): validate cancellation overload result * fix(analyzers): tolerate dynamic assembly scans * fix(analyzers): refine hierarchy and LINQ checks * fix(analyzers): validate inferred types * refactor(analyzers): simplify task join scan * fix(analyzers): close review gaps * fix(analyzers): tighten async safety analysis * fix(analyzers): refine callable safety checks * fix(analyzers): align instance registration Keep traced concrete instances out of the dependency-closure seed while still treating them as directly registered. Extract cancellation-flow predicates to clear the complexity gate without behavior changes. * fix(analyzers): close callback flow gaps Recognize direct IModule DI registrations and linked-token arrays while traversing eager and task-returning callbacks. * fix(analyzers): cover async enumeration Analyze await foreach cancellation flow and infer scanned assemblies only from assembly-producing expressions. * fix(analyzers): follow callback method groups Track type-based IModule service registrations and task returns from local-function callbacks. * refactor(analyzers): split DI tracing * fix(analyzers): trace descriptor registrations Follow delegate locals into callback returns and recognize ServiceDescriptor module factories. * fix(analyzers): trace concrete registrations * fix(analyzers): isolate traversal branches * fix(analyzers): trace eager callbacks * fix(analyzers): refine control-flow tracing * fix(analyzers): refine async token flow * refactor(analyzers): split awaited controls * fix(analyzers): extend async flow tracing * fix(analyzers): refine branch and index flow * fix(analyzers): refine flow analysis * refactor(analyzers): split LINQ consumption * fix(analyzers): trace descriptor and task flows * fix(analyzers): cover docs and spreads * fix(analyzers): refine registration async flow * fix(analyzers): tighten flow tracking Refs #3314 * fix(analyzers): complete registration tracing Refs #3314 * fix(analyzers): follow execution helpers Refs #3314 * refactor(analyzers): simplify execution lookup * fix(analyzers): complete helper tracing Refs #3314 * fix(analyzers): keep registration diagnostics Unknown registration shapes must not suppress otherwise actionable module diagnostics. Introduce the new analyzer rules as warnings to avoid upgrade-time build breaks. * fix(analyzers): close tracing gaps * refactor(analyzers): split member traversal * fix(analyzers): trace helper values * fix(analyzers): track startup registration flow * refactor(analyzers): split flow predicates * fix(analyzers): scope token mappings * fix(analyzers): close tracing gaps * fix(analyzers): complete flow tracing * fix(analyzers): trace branch assignments * fix(analyzers): trace branch values * refactor(analyzers): reduce flow complexity * refactor(analyzers): simplify value lookup * fix(analyzers): trace switch values * fix(analyzers): track possible branch values Refs #3314 * fix(analyzers): handle dynamic module arrays Refs #3314 * fix(analyzers): trace reachable execution paths Refs #3314 * fix(analyzers): close reachability gaps Refs #3314 * fix(analyzers): trace remaining source flows Close review gaps in callback forwarding, constant switches, conditional cancellation flow, branch-assigned types, and abstract dependencies. Refs #3314 * refactor(analyzers): simplify callback tracing Extract invocation and forwarding checks so callback reachability stays below the complexity gate without changing behavior. Refs #3314 * fix(analyzers): close reachability gaps * fix(analyzers): close reachability gaps * fix(analyzers): track dynamic DI registrations Recognize Replace descriptors and conservatively suppress registration diagnostics when an IModule implementation type is runtime-computed. Refs #3314 * fix(analyzers): trace startup helper flows Handle dead switch-expression registrations and follow source helper returns for ServiceDescriptor and CancellationToken analysis. Add coverage for inline startup lambdas. * refactor(analyzers): reduce complexity * fix(analyzers): preserve release history * fix: cover analyzer reachability gaps * refactor(analyzers): reduce flow complexity Extract service-descriptor return handling and cancellation-flow collection so CodeFactor no longer rejects the analyzer batch. * fix: close analyzer reachability gaps Track initializer registrations through constructors and evaluate constant and local callback branches so unreachable code cannot satisfy module registration or token flow. * refactor(analyzers): split instance flow branches Extract anonymous-function and conditional registration tracking from the complex dispatcher without changing analysis behavior. * fix(analyzers): follow selected branches * fix(analyzers): trace switch and field flows * fix(analyzers): trace static initialization * fix(analyzers): align nullable test stubs * fix(engine): align timeout result type * fix(analyzers): close reachability gaps * fix(tests): follow renamed builder API * fix(analyzers): close flow edge cases * fix(analyzers): close registration flow gaps * fix(analyzers): trace reachable helper flow * fix(analyzers): cover nested flow branches * refactor(analyzers): reduce flow complexity * fix(analyzers): close registration gaps * fix(analyzers): close registration flow gaps * fix(analyzers): close reachability gaps * fix(analyzers): follow stored registrations * fix(analyzers): isolate descriptor returns Clone cycle guards per reachable helper return. Refs #3314. * fix(analyzers): close review flow gaps * refactor(analyzers): lower path complexity * fix(analyzers): close flow analysis gaps * fix(analyzer): follow event handlers * fix(analyzer): close registration gaps * fix(gate): paginate review threads Fetch every review-thread page and deny safely on malformed or repeated cursors so large PRs can be evaluated without bypassing unresolved feedback. * fix(analyzers): track factories and docs * fix(analyzer): trace helper member writes * fix(analyzers): close callback gaps * fix(analyzers): close registration gaps * fix(analyzers): resolve member-backed DI types * fix(analyzers): close factory and ID gaps * fix(docs): preserve analyzer severities * fix(analyzers): require event subscription * fix(analyzers): ignore event removals * fix(analyzers): exempt delegated factories * fix(analyzers): recognize root builder APIs * fix(analyzers): follow fluent registrations
Summary
Validation
Stack
This PR is based on #3433 so its seven new analyzer rules can receive MP0013-MP0019 without duplicating that PR in the review diff.
Closes #3312