Detect common module authoring mistakes - #3433
Conversation
Code reviewReviewed the new
Nothing else stood out — the concurrent-execution handling ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1527707c7
ℹ️ 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 both review points in 5dc7a0a: split the batch into ModuleRegistrationAnalyzer, ModuleAsyncSafetyAnalyzer, and DuplicateDependsOnAnalyzer while retaining shared low-level analysis helpers; routed all seven descriptors through DiagnosticDescriptorFactory with localized Resources.resx title/message/description entries. Validation: analyzer tests 67/67 passed; touched-file analyzer formatting passed. @codex review |
There was a problem hiding this comment.
Code review (follow-up on 5dc7a0a)
Re-reviewed after the split/refactor commit. Both points from the previous review are properly addressed:
- Single-class → three focused analyzers.
ModuleRegistrationAnalyzer(MPREG001/MPTYPE001),ModuleAsyncSafetyAnalyzer(MPASYNC001-004), andDuplicateDependsOnAnalyzer(MPDEP004) now each own their own rule set andSupportedDiagnostics, matching the one-analyzer-per-concern convention used bySelfDependencyAnalyzer,StatefulModuleAnalyzer, etc. The shared low-level walk logic stays inModuleAuthoringAnalysis(an internal static helper, not itself aDiagnosticAnalyzer), which is a reasonable way to keep the concurrent-tracking/operation-walking code from being duplicated three times. - Routed through
DiagnosticDescriptorFactory. All 7 descriptors now go through the shared factory withResources.resxentries instead of hardcoded literals, and the release notes/severities inAnalyzerReleases.Unshipped.mdline up with what's actually configured in code (e.g. MPASYNC001/MPDEP004 correctly default toErrorvia the factory's default, the rest are explicitWarning).
One new inconsistency introduced by this fix, though: every other analyzer in the project passes the resource name as nameof(Resources.XyzTitle) (compile-time-checked), but the 3 new analyzers pass raw string literals instead, e.g.:
// ModuleRegistrationAnalyzer.cs
DiagnosticDescriptorFactory.Create(
UnregisteredModuleId,
"UnregisteredModuleAnalyzerTitle", // string literal
"UnregisteredModuleAnalyzerMessageFormat",
"UnregisteredModuleAnalyzerDescription",
severity: DiagnosticSeverity.Warning);vs. every existing analyzer, e.g. AsyncModuleAnalyzer.cs:
DiagnosticDescriptorFactory.Create(
DiagnosticId,
nameof(Resources.AsyncModuleAnalyzerTitle), // nameof
nameof(Resources.AsyncModuleAnalyzerMessageFormat),
nameof(Resources.AsyncModuleAnalyzerDescription));This isn't cosmetic — Resources.Designer.cs was not regenerated in this PR (grep for the 7 new keys, e.g. UnregisteredModuleAnalyzerTitle, finds zero hits in Resources.Designer.cs), so nameof(Resources.UnregisteredModuleAnalyzerTitle) would not have compiled as-is; the string-literal form was almost certainly chosen to route around that. The trade-off is losing the exact compile-time safety net the factory-routing recommendation was meant to restore: a future rename/typo of one of these 7 .resx keys will silently produce a blank/missing diagnostic title instead of a build break, and Resources.Designer.cs is now out of sync with Resources.resx (it's missing 7 members that IDE tooling and other consumers would expect to see). Recommend regenerating Resources.Designer.cs (re-run the ResX custom tool, or add the members by hand to match the generator's output) and switching these 7 call sites to nameof(Resources.X) so all 17 descriptors in the project follow the same pattern.
Nothing else stood out on a re-check of the analysis logic itself: the cancellation-overload matching (IsCancellationOverload) correctly rejects the Call(string) vs Call(int, CancellationToken) case in the new tests by comparing non-token parameter types positionally, the ConfigureAwait unwrap loop in GetAwaitedInvocation handles chained calls, and the ConcurrentBag + Interlocked.Exchange/Volatile.Read combination for cross-symbol-action registration tracking is race-free (the ref capture into the compilation-end lambda is evaluated lazily at invocation time, not at registration time, so it correctly reflects the final value). Test coverage (13 test methods across the three analyzers) covers the meaningful branches: registered/unregistered, public/non-public, all three blocking-call forms, direct and ConfigureAwait-wrapped unflowed tokens, linked/derived tokens, unrelated overloads, and duplicate DependsOn.
Minor, non-blocking observation: MPASYNC001 (async void) fires for any async-void method inside a module class, not just ones invoked from ExecuteAsync. If modules ever wire up event handlers (the idiomatic case where async void is intentional, e.g. (object sender, EventArgs e) signatures), this would flag a legitimate pattern — worth keeping in mind if that comes up in practice, but not something to block on given modules don't appear to use event handlers today.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5dc7a0ad3d
ℹ️ 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".
5dc7a0a to
c2f8665
Compare
There was a problem hiding this comment.
Code review
Re-reviewed at the current HEAD (commit c2f8665457). The three-analyzer split, shared ModuleAuthoringAnalysis helper, and factory-routed descriptors from the last round still look good — nice separation of concerns (registration vs. async-safety vs. duplicate-dependency), and the cancellation-token detection is genuinely careful (handles ConfigureAwait chains, linked/derived tokens, and same-name overloads with unrelated parameter types via IsCancellationOverload).
Two things remain from my read:
1. Previous finding not yet addressed: nameof vs. string literals for resource keys.
All 7 new descriptors in ModuleRegistrationAnalyzer.cs, ModuleAsyncSafetyAnalyzer.cs, and DuplicateDependsOnAnalyzer.cs still pass raw string literals (e.g. "UnregisteredModuleAnalyzerTitle") instead of nameof(Resources.UnregisteredModuleAnalyzerTitle), unlike every other analyzer in the project (AsyncModuleAnalyzer, SelfDependencyAnalyzer, etc.), which use nameof. This is because Resources.Designer.cs hasn't been regenerated — grep for the 7 new keys against it still returns zero hits, so nameof genuinely wouldn't compile as written. The trade-off: Resources.Designer.cs is now out of sync with Resources.resx (IDE tooling/consumers won't see these 7 members), and a future rename/typo of one of these resx keys will silently produce a blank diagnostic title instead of a compiler error. Recommend regenerating Resources.Designer.cs (rerun the ResX custom tool or add the members by hand matching the generator's existing output shape) and switching these 7 call sites to nameof.
2. Test gap: assembly-wide and params-Type[] registration forms are implemented but untested.
The PR description calls out tracking "generic, params-Type, and assembly-wide module registration forms," and ModuleAuthoringAnalysis.TrackRegistrationInvocation does branch on AddModules(params Type[]), AddModulesFromAssembly, and AddModulesFromAssemblyContainingType<T> (all real APIs in PipelineBuilderExtensions). But ModuleAuthoringAnalyzerTests.cs only exercises the generic AddModule<T>() path (Does_Not_Report_Registered_Module). There's no test for:
AddModules(typeof(A), typeof(B))(theITypeOfOperation-scanning branch), orAddModulesFromAssembly(...)/AddModulesFromAssemblyContainingType<T>()— which take the special path of suppressing allMPREG001diagnostics compilation-wide once seen (assemblyRegistrationUsed).
That suppression is an all-or-nothing, whole-compilation behavior — exactly the kind of logic most likely to regress silently without a test pinning it down (e.g. a future refactor could flip the flag check and start reporting false positives for every module in an assembly-scanned project). Worth adding at least one test per form, especially one confirming that an assembly-registered module (no explicit AddModule<T>() call) is not flagged.
Minor / non-blocking: the AddModules/typeof detection is syntactic (DescendantsAndSelf().OfType<ITypeOfOperation>()), so it won't see indirection like Type[] types = { typeof(A) }; builder.AddModules(types); — that would still be flagged as "unregistered." Given MPREG001 defaults to Warning, this seems like an acceptable heuristic trade-off, but might be worth a one-line comment noting the limitation so it isn't rediscovered as a "bug" later.
Nothing here blocks merging on functional grounds — the analyzer test suite (67 passing per the PR description) covers the core detection logic well. The two items above are about closing out the previous round's feedback and hardening test coverage for functionality the PR explicitly claims to support.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2f8665457
ℹ️ 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 the follow-up review in 440c3ea:
Validation: analyzer solution Release build succeeded (0 errors; 17 existing release-tracking warnings), 79/79 analyzer tests passed, and touched-file severity-info formatting passed. @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 440c3ea7f1
ℹ️ 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02e044fc08
ℹ️ 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 (re-review at HEAD 02e044fc08)
Both items from my last review (commit c2f8665457) are now resolved:
nameofvs. string literals — all 7 descriptors inModuleRegistrationAnalyzer,ModuleAsyncSafetyAnalyzer, andDuplicateDependsOnAnalyzernow usenameof(Resources.X), andResources.Designer.cshas been regenerated with the matching members (verified all 7 new keys are present).- Test gap for registration forms —
ModuleAuthoringAnalyzerTests.csnow coversAddModules(typeof(...))(params-Type[]),AddModulesFromAssemblyContainingType<T>(),AddModulesFromAssembly(...), and an explicitExternal_Assembly_Scan_Does_Not_Hide_Unregistered_Local_Moduletest that pins down the all-or-nothing suppression behavior I flagged as the highest-risk-of-silent-regression path.
New since last review: scope of the PR has grown substantially
Commit 02e044fc08 ("Unify analyzer diagnostic IDs and documentation (#3437)") adds something well beyond the PR's stated purpose. The title/description/linked issue (#3314) are about detecting module-authoring mistakes, but this commit renumbers every pre-existing analyzer in the project — including ones this PR never touches, like AwaitThis, StatefulModule, MPDEP001-003, ConsoleUse, LoggerInConstructor, EnumerableModuleResult, MissingDependsOnAttribute, and the unrelated ModularPipelines.Development.Analyzers package (MPD0001/MPD0002) — into a new MP0001-MP0019 scheme, plus adds a whole docs-generation pipeline (docs/scripts/generate-analyzer-docs.mjs, 19 generated rule pages, docs/package.json changes).
This is a real breaking change for anyone with .editorconfig severity overrides or #pragma warning disable ConsoleUse/AwaitThis/etc. in their code — confirmed by the fact that this repo's own src/ModularPipelines.Build/Program.cs needed a pragma update (ConsoleUse → MP0004) as part of the same commit. To the PR's credit, it's not half-done: the migration is documented with a full legacy-ID → new-ID table in docs/docs/how-to/analyzers.md, and AnalyzerMetadataTests.PublicRulesUseUnifiedIdsAndHelpLinks pins the full ID sequence with no gaps or collisions.
Given the breaking-change nature and that it's unrelated to "detect module authoring mistakes," I'd suggest this ID-unification work land as its own PR with its own description/changelog entry rather than riding in on this one — it makes the diff harder to review for what's actually new analyzer logic vs. a mechanical rename, and ties an unrelated breaking change to this feature's release notes. Not a blocker since it's already correct and tested, but worth considering for the next PR of this shape.
Minor, non-blocking: the MP#### ID space is now shared across two independently-versioned packages (ModularPipelines.Analyzers owns 1-10 and 13-19, ModularPipelines.Development.Analyzers owns 11-12), but there's no single allocator — the reserved ranges only exist as a hardcoded Enumerable.Range split in AnalyzerMetadataTests.cs. A future rule added to either package without checking the other's range could collide silently (the test would fail, but only if someone remembers to update it in both places). A short comment at the top of each DiagnosticId block noting the reserved range, or a shared constants file, would make this more self-documenting.
Nothing else stood out — the core detection logic in ModuleAuthoringAnalysis.cs is unchanged since the last round (already reviewed in depth: cancellation-overload matching, ConfigureAwait unwrapping, concurrent registration tracking all check out), and the new DiagnosticDescriptorFactory.Create(..., helpLinkUri: ...) addition correctly threads through to every descriptor per AnalyzerMetadataTests.
|
Fixed the Analyzer Diagnostics CI failure in 44744a9. RS1038 was triggered because this internal project intentionally co-locates its analyzers and CodeFixProvider types (and therefore references Workspaces); the project now suppresses only RS1038 while retaining all other extended analyzer rules. Validation: ModularPipelines.Analyzers.sln Release build completed with 0 errors; development analyzer tests passed 30/30. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 44744a9aca
ℹ️ 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 (re-review at HEAD 44744a9aca)
Went through the full history on this PR (five prior rounds from claude/codex bots) against the current HEAD, and independently re-verified the fixes rather than trusting the commit messages:
- Auto-registered required dependencies —
AddRequiredDependencyClosure(ModuleAuthoringAnalysis.cs:322-341) now walks the non-optional[DependsOn<T>]closure and adds it to the registered set before reporting MP0013, matchingModuleAutoRegistrar's runtime behavior. Covered byDoes_Not_Report_Required_AutoRegistered_Dependency. - Reusable-library false positives — MP0013 is now gated behind
IsApplication(context.Compilation.Options.OutputKind), so class libraries whose modules are registered by a downstream consumer no longer get flagged. Covered byDoes_Not_Require_Registration_In_Reusable_Library. - Assembly-scan suppression scope — this moved from the previous all-or-nothing boolean flag to a real
scannedAssembliesset keyed by resolvedIAssemblySymbol, so an unrelatedAddModulesFromAssemblycall no longer blanket-suppresses local unregistered-module warnings.External_Assembly_Scan_Does_Not_Hide_Unregistered_Local_Modulepins this down. nameofvs. string literals /Resources.Designer.csdrift — resolved, all 7 descriptors usenameof(Resources.X)and the designer file is regenerated.- Confirmed
[with(SymbolEqualityComparer.Default)]atModuleAuthoringAnalysis.cs:463isn't a typo — it's the existing "collection expression arguments" preview pattern already used inModuleConfigurationBuilder.cs, consistent with this project'sLangVersion=preview. - The final commit (
44744a9aca) is a minimal, well-justifiedNoWarnfor RS1038 with a comment explaining why this internal package intentionally co-locates analyzers and code fixes.
One remaining gap: MP0018 (non-public module) doesn't account for assembly-scan registration
ReportModuleDiagnostics (ModuleAuthoringAnalysis.cs:283-290) reports NonPublicModuleRule for every non-public module except ones in instanceRegisteredModules (i.e. registered via AddModule(instance)/AddModule(factory)):
foreach (var module in moduleSet.Where(module =>
!IsPublic(module) && !instanceRegistered.Contains(module)))
{
ReportModuleDiagnostic(context, module, ModuleRegistrationAnalyzer.NonPublicModuleRule);
}This runs before scannedAssemblies is even computed (that set is built a few lines later, only for the MP0013 check). But ServiceCollectionExtensions.AddModulesFromAssembly (src/ModularPipelines/Extensions/ServiceCollectionExtensions.cs:272) discovers modules via assembly.GetTypes() — not GetExportedTypes() — so it legitimately picks up internal module classes at runtime. An internal module living in an assembly that's registered via AddModulesFromAssembly(...) / AddModulesFromAssemblyContainingType<T>() is fully functional, but MP0018 will still flag it as "module class is not public," since that check only exempts the instance/factory forms, not the assembly-scan form.
This is the same class of false positive that was just fixed for MP0013 (reusable-library / assembly-scan registration), applied to a sibling rule that shares the same scannedAssemblies data but doesn't consult it. Confirmed there's no test for this combination — ModuleSource(...) (the shared test helper) always emits public class BuildModule, so none of the AddModulesFromAssembly* tests exercise a non-public module.
Suggested fix: move the MP0018 loop after scanned is computed and also exempt scanned.Contains(module.ContainingAssembly), then add a test pairing an internal module with AddModulesFromAssembly/AddModulesFromAssemblyContainingType.
Nothing else stood out — this is genuinely the last loose end after a very thorough back-and-forth; everything else checks out against the actual runtime registration/discovery code, not just the test suite.
|
Addressed all fresh review findings in
Validation: analyzer tests 91/91 passed; touched-file info analyzer and whitespace gates passed; All seven inline threads are answered and resolved. @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81edd6aa85
ℹ️ 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".
Code review (re-review at HEAD
|
|
Follow-up @codex review |
|
@codex review |
|
Addressed the review-body scope concern: the PR description now explicitly discloses that merged stacked PR #3437/#3312 is included and that renumbering shipped analyzer IDs breaks ID-keyed suppressions, with migration guidance. Latest P2 fixes are in @codex review |
Code review (HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 240b06c194
ℹ️ 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".
|
@codex review |
Code review (HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7da253b0cc
ℹ️ 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81bae8fba7
ℹ️ 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".
Code review (delta since last review, HEAD
|
|
@codex review |
Code review (delta since last review, HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 581df7be1a
ℹ️ 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".
|
Fixed the delegated-factory MP0018 false positive in 144a324. Unresolved DI factory registrations are now tracked separately: they conservatively suppress the non-public diagnostic while unrelated unresolved dynamic Type[] registrations still do not. Added an internal-module delegated-factory regression; focused pair passed 2/2; analyzer Release build passed with 0 warnings/errors. @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 144a324bc4
ℹ️ 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".
Code review (delta since last review, HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e7482238fd
ℹ️ 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".
Code review (delta since last review, HEAD
|
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Code review (delta since last review, HEAD
|
|
Required pipeline failed on unrelated RunReportTests timing flake: DistributedMasterReconcilesMatchedMetricsPerWorker timed out; all other jobs passed. Triggered one failed-job rerun. |
Summary
Thread.Sleep, unflowed cancellation, and duplicateDependsOndeclarationsType, assembly-wide, instance, factory, andServiceDescriptormodule registrations across reachable codeBreaking change
This PR also carries #3312. It renumbers the ten previously shipped analyzer IDs to the
MP####family. Existing#pragma warning disable <old-id>anddotnet_diagnostic.<old-id>.severityentries must migrate to the new IDs; for example,MPDEP002becomesMP0005. The release metadata and analyzer migration guide record every mapping.Validation
Closes #3314