Skip to content

Add Kafka publication partitioner analyzer and code fixes - #4255

Draft
lillo42 wants to merge 12 commits into
masterfrom
add.kafka.analyzer
Draft

Add Kafka publication partitioner analyzer and code fixes#4255
lillo42 wants to merge 12 commits into
masterfrom
add.kafka.analyzer

Conversation

@lillo42

@lillo42 lillo42 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a Kafka publication partitioner analyzer with three new diagnostics:

  • BRT006 — warns when a KafkaPublication is created without an explicit Partitioner assignment.
  • BRT007 — warns when Partitioner.ConsistentRandom is used and recommends Partitioner.Murmur2Random for new publications.
  • BRT008 — warns when Partitioner.Consistent is used and recommends Partitioner.Murmur2 for new publications.

This also introduces a new Paramore.Brighter.Analyzer.CodeFixes project and ships it in the analyzer NuGet package. The code fixes can add the recommended Partitioner.Murmur2Random assignment or replace legacy ConsistentRandom/Consistent values with the recommended Murmur2 alternatives.

Documentation for BRT006–BRT008 is included, together with analyzer and code-fix tests.

Related Issues

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the Contributing Guide
  • I have checked the documentation for relevant guidance
  • I have added/updated XML documentation for any public API changes
  • I have added/updated tests as appropriate
  • My changes follow the existing code style and conventions

Additional Notes

Existing publications that intentionally rely on ConsistentRandom or Consistent can keep their current partition assignment and suppress the warning if preserving the current key-to-partition mapping is required.

Maintainer decisions

Two points raised in review are deliberate decisions, not oversights:

  • BRT006 ships at Warning, enabled by default. Every KafkaPublication created without an explicit Partitioner is flagged, so consumers building with TreatWarningsAsErrors (this repo included) will see build failures on upgrade until they either set the partitioner explicitly or suppress the rule. This is intentional: omitting the assignment silently selects the ConsistentRandom default — the same value BRT007 discourages — so it is reported at the same severity (the argument is written up in BRT006.md).
  • The code fixes change runtime behaviour, and Fix All is deliberately not offered. Applying a fix moves a publication to a Murmur2 partitioner, which re-partitions the topic — keys map to different partitions than before. Because of that, the fixes can only be applied one diagnostic at a time: each change must be an explicit, per-site decision by the developer, not a solution-wide action. Each diagnostic is individually suppressible for publications that must keep their current assignment, and the caveat is stated in the BRT006–BRT008 docs and in the code action title.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Code Review — Add Kafka Analyzer (BRT006–BRT008)

Thanks for this — the three Kafka partitioner rules plus code fixes are a genuinely useful addition, the docs (BRT006–008) are excellent, and the test coverage for the happy-path and single-diagnostic cases is solid. A few things worth addressing before merge, one of which I believe is a real correctness bug.

🔴 Correctness — visitor recurses into every object creation, causing false positives at the wrong location

KafkaPublicationPartitionerVisitor.VisitObjectCreation calls base.VisitObjectCreation(operation) unconditionally:

public override void VisitObjectCreation(IObjectCreationOperation operation)
{
    if (operation.Type!.Accept(new ChildOfVisitor(KafkaPublicationClassName, ...)))
    {
        PublicationName = operation.Type.Name;
        IsKafkaPublication = true;
    }

    // base walks the children ... ALWAYS, even when this operation is not a KafkaPublication
    base.VisitObjectCreation(operation);
}

Compare with the existing sibling RequestTypeAssignmentVisitor, which deliberately calls base.VisitObjectCreation(operation) only inside the if (IsPublicationType) guard. Because OperationWalker descends into an object creation's constructor arguments and initializers, the unconditional recursion means that when the analyzer fires on some outer (non-Kafka) object creation, the walker descends into any KafkaPublication nested inside it and sets IsKafkaPublication / IsPartitionerAssigned / IsConsistent… from that nested node. The diagnostic is then reported at the outer operation's location.

Concrete failure — a very common Brighter registration shape:

var factory = new KafkaProducerRegistryFactory(
    configuration,
    new[] { new KafkaPublication() });   // partitioner missing
  • The analyzer fires on the inner new KafkaPublication() → correct BRT006 at the right location. ✅
  • The analyzer also fires on the outer new KafkaProducerRegistryFactory(...); the walker descends into the argument array, finds the nested KafkaPublication, sets the flags, and reports BRT006 a second time, anchored to the factory expression. ❌ (duplicate + wrong location)

The same happens with Partitioner = Partitioner.Consistent nested inside any wrapping object creation → duplicate BRT008 on the wrapper.

Fix: move base.VisitObjectCreation(operation) inside the if block (mirroring RequestTypeAssignmentVisitor) so the walk only descends when the operation itself is the KafkaPublication being analyzed. As a bonus this also avoids walking the full subtree of every object creation in the compilation (a small perf win).

🟠 Test coverage gap

All analyzer tests declare the publication at statement level (var publication = new KafkaPublication { … };), so none exercise a KafkaPublication nested inside another object creation (constructor argument, array, or object initializer) — exactly the case the bug above breaks. Please add a test with a nested publication (e.g. passed into a KafkaProducerRegistryFactory) asserting a single diagnostic at the publication's location.

🟡 Minor points

  • Doc/severity mismatch (BRT006.md): the code example comment still reads // Info: Partitioner assignment is missing, but the rule ships at Warning (and the doc body explicitly argues for Warning over Info). Update the comment to // Warning: to match.
  • Public field naming: the DiagnosticDescriptor fields use the s_ prefix (s_missingPartitionerRule), which is the convention for private static fields. The existing analyzers expose these as PascalCase (RequestTypeMissingRule, WrongRequestTypeRule). Consider matching for consistency, since they're public and referenced from tests.
  • Code fix doesn't guarantee Partitioner resolves: both code fixes insert Partitioner.Murmur2Random / Partitioner.Murmur2 as an unqualified type reference, which only compiles when the document has using Paramore.Brighter.MessagingGateway.Kafka;. If a user references KafkaPublication fully-qualified without that using, the fix produces uncompilable code. Consider emitting a qualified name with Simplifier.Annotation (and/or an add-import) so it resolves regardless of usings.
  • Release tracking: BRT006–008 were appended to AnalyzerReleases.Shipped.md under the existing ## Release 1.0 section, while AnalyzerReleases.Unshipped.md is empty. New (not-yet-released) rules conventionally go in Unshipped first. Minor and consistent with how the repo has handled it so far, but worth confirming that's intended.
  • Trailing newline: KafkaPublicationPartitionerAnalyzerTest.cs is missing a final newline.

Nice work overall — the correctness fix is small and the rest are polish. 👍

🤖 Automated review. I verified the visitor-recursion difference against the existing RequestTypeAssignmentVisitor; I was unable to run a build in this environment, so please confirm the nested-publication behaviour with the suggested test.

@lillo42 lillo42 changed the title Add Kafka Analyzer Add Kafka publication partitioner analyzer and code fixes Jul 24, 2026
@lillo42 lillo42 self-assigned this Jul 26, 2026
@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer (BRT006–BRT008)

Nice addition — the rules are well-motivated, the docs (BRT006/7/8.md) are genuinely educational rather than boilerplate, and splitting code fixes into their own assembly is the right structure. The analyzer follows the existing PublicationRequestTypeAssignmentAnalyzer shape closely, which makes it easy to follow.

A few things I'd like to see addressed before merge, plus some smaller notes.


Correctness / potential bugs

1. Partitioner is matched by name only, on any nested initializerKafkaPublicationPartitionerVisitor.cs:57-58

if (operation.Target is IPropertyReferenceOperation propertyReference &&
    propertyReference.Property.Name == BrighterAnalyzerGlobals.PartitionerProperty)

Because VisitObjectCreation calls base.VisitObjectCreation(operation), the walker descends through the whole initializer subtree, so any Partitioner = ... on any nested object satisfies this check. That's not hypothetical in this codebase — Confluent.Kafka.ProducerConfig has a Partitioner property too (see KafkaMessageProducer.cs:133 and tests/Paramore.Brighter.Kafka.Tests/.../When_recieving_a_message_without_partition_key_header.cs:45). A nested config object would suppress BRT006 (false negative) or trigger BRT007/BRT008 attributed to the wrong node.

Two options:

  • Guard on the containing type: propertyReference.Property.ContainingType.Accept(new ChildOfVisitor(KafkaPublicationClassName, KafkaMessagingGatewayAssembly)).
  • Simpler and more robust: drop the walker for this rule and inspect operation.Initializer?.Initializers directly on the IObjectCreationOperation. You only care about assignments in this object's initializer, so the tree walk buys nothing and costs you this class of bug (and the nested-KafkaPublication case the comment at lines 46-50 is worried about).

2. False positive when Partitioner is set after constructionKafkaPublicationPartitionerVisitor.cs:39-53

var publication = new KafkaPublication { Topic = new RoutingKey("x") };
publication.Partitioner = Partitioner.Murmur2Random;   // BRT006 still fires

Only the object-creation operation is analysed, so this common pattern (and configuring a publication inside a helper method) reports a warning the user has already fixed. Worth either handling the enclosing block / variable-initializer case, or at minimum documenting the limitation in BRT006.md alongside the suppression guidance.

3. The BRT007/8 code fix can emit code that doesn't compilePartitionerValueCodeFixProvider.cs:88-92

ExpressionSyntax newValue = assignment.Right switch
{
    MemberAccessExpressionSyntax memberAccess => memberAccess.WithName(newName),
    _ => newName
};

GetPartitionerValueName unwraps IConversionOperation, which covers explicit casts too — so Partitioner = (Partitioner)Partitioner.Consistent is flagged, and the _ arm rewrites it to a bare Partitioner = Murmur2 that won't bind. The fallback is only safe when the original was an unqualified identifier (using static). I'd suggest matching IdentifierNameSyntax explicitly and bailing out (not registering a fix) for anything else, or reusing the qualify-then-Simplifier approach from MissingPartitionerCodeFixProvider.

This is exactly the class of bug that CompilerDiagnostics.None in BaseCodeFixTest.cs:25 hides — see the test note below.

4. NullabilityGetPartitionerValueName (KafkaPublicationPartitionerVisitor.cs:76-85) is declared to return string but returns null, and PublicationName (line 37) is a non-nullable auto-property assigned only conditionally. The repo has Nullable=enable + TreatWarningsAsErrors=true (src/Directory.Build.props:17). The PublicationName pattern is copied from the existing RequestTypeAssignmentVisitor.cs:37 so it evidently builds today, but string? on both would be more honest and costs nothing.


Consumer impact

BRT006 fires on every KafkaPublication construction, at Warning, isEnabledByDefault: true. For consumers building with TreatWarningsAsErrors (as this repo itself does), upgrading the analyzer package breaks the build until every publication is touched. The PR is marked "non-breaking" — I'd either:

  • ship BRT006 at DiagnosticSeverity.Info (BRT007/8 stay at Warning, since they flag an explicit choice), or
  • keep Warning but call it out prominently in release_notes.md / the package release notes.

Relatedly: the BRT006 code fix changes runtime behaviour. The property default is ConsistentRandom (KafkaPublication.cs:104), so applying the fix moves an existing publication from ConsistentRandom to Murmur2Random and re-partitions the topic. BRT007.md and BRT008.md both carry the "existing publications can safely ignore this" caveat; BRT006.md does not, even though it has the same consequence — and a Fix All across a solution would do it silently to every publication. Please add the same warning to BRT006.md.

None of the descriptors set helpLinkUri, so the nicely written docs aren't reachable from the IDE lightbulb. Pre-existing for BRT001–005, but these three are the ones where the "why" really matters.


Packaging

Paramore.Brighter.Analyzer.Package.csproj:33 ships Paramore.Brighter.Analyzer.CodeFixes.dll into analyzers/dotnet/cs. That assembly references Microsoft.CodeAnalysis.CSharp.Workspaces, which the IDE host provides but csc does not. Worth consuming the packed .nupkg from a scratch project and doing a command-line dotnet build to confirm no CS8032/CS8033 warnings surface. A verification step, not necessarily a change.

Minor: Paramore.Brighter.Analyzer.CodeFixes.csproj sets Authors, Description, PackageTags and DevelopmentDependency alongside IsPackable=false — all no-ops. Authors also says "Rafael Andrade" while the analyzer project says "Aboubakr Nasef"; worth a consistency pass.

Also: new rules conventionally land in AnalyzerReleases.Unshipped.md (currently empty) and get promoted to Shipped.md at release time. Adding BRT006–008 straight into the Release 1.0 table in Shipped.md works — just confirm that's the intent.


Test coverage

Happy paths are covered well and the nested-object-creation cases are a good inclusion. Gaps I'd want filled, mostly because they're the paths where the bugs above live:

  • CompilerDiagnostics.None in BaseCodeFixTest.cs:25 means the fixed code is never verified to compile. For code-fix tests specifically I'd raise this to CompilerDiagnostics.Errors — otherwise the ParseName + Simplifier.Annotation machinery in MissingPartitionerCodeFixProvider.cs:84-103 is untested for the case it exists to serve.
  • No test for MissingPartitionerCodeFixProvider without using Paramore.Brighter.MessagingGateway.Kafka; in scope. That's the entire reason the fix emits a fully-qualified name then reduces it, and it's the one scenario not exercised.
  • No negative test that Partitioner.Random, or a variable/const expression, produces no diagnostic.
  • No negative test that a plain Publication, or another type exposing a Partitioner property, is ignored — that's issue Support for multiple Application Layer Protocols in Task Queues #1.
  • No test for the "set after construction" pattern (issue No API Documentation #2), whichever way you resolve it.
  • The repeated testContext.TestState.AdditionalReferences.Add(...typeof(KafkaPublication)...) in every test in KafkaPublicationPartitionerAnalyzerTest.cs could move into a small base class, the way BaseCodeFixTest does it.

Nits

  • BrighterAnalyzerGlobals.cs and DiagnosticsIds.cs were converted to file-scoped namespaces (and lost their BOM) in the same commit as the behavioural change. CLAUDE.md asks for structural and behavioural changes to be separated (/tidy-first) — not worth reverting now, but it makes the diff noisier than it needs to be.
  • KafkaMessagingGatewayAssembly and KafkaMessagingGatewayNamespace are the same literal declared twice — easy for the two to drift.
  • MissingPartitionerCodeFixProvider.cs:99 uses Formatter.Format(..., document.Project.Solution.Workspace, ...); await Formatter.FormatAsync(document.WithSyntaxRoot(newRoot), Formatter.Annotation, cancellationToken: ct) avoids the Workspace hop and picks up the document's .editorconfig formatting options.
  • New public types carry no XML docs (consistent with the existing analyzers, and that checklist box is unticked — just confirming it's deliberate).
  • BRT006 on KafkaPublication<MyRequest> renders as "KafkaPublication" because it uses Type.Name; ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) would read better.

Overall the rules, docs, and structure are solid. Items 1 and 3 are the ones I'd treat as blocking, and I'd settle the BRT006 severity / "non-breaking" question before this ships.

🤖 Review by Claude Code

- Only treat Partitioner assignments on KafkaPublication itself as the
  publication's partitioner (a nested object's own Partitioner property,
  e.g. Confluent's ProducerConfig, no longer suppresses BRT006)
- Don't report BRT006 when the partitioner is assigned to the same local
  later in the same block
- Don't offer the BRT007/8 code fix when the assigned expression is
  neither a member access nor a bare identifier (would not compile)
- Verify fixed code compiles in code-fix tests (CompilerDiagnostics.Errors)
- Add tests: Partitioner.Random negative, nested config with its own
  Partitioner, set-after-construction, code fix without the Kafka using
- Document in BRT006.md that the code fix changes partition assignment
  and note the helper-method limitation
- Add helpLinkUri for BRT006-BRT008, render generic publications as
  KafkaPublication<T>, drop no-op packaging metadata from CodeFixes.csproj
@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer and code fixes (1/2)

Overall this is a well-put-together contribution. The analyzer follows the existing Analyzer + Visitors/Operation + ChildOfVisitor shape already established by BRT001–BRT005, the diagnostic IDs and release table are updated, and the three docs pages are genuinely good — they explain why (hot partitions, single-consumer-per-partition, interop with standard Kafka clients) rather than just what, and they're honest about the fact that changing the partitioner re-partitions the topic. Adding helpLinkUri is an improvement over the existing rules, which don't set it. CompilerDiagnostics.Errors in BaseCodeFixTest is the right call — it means a code fix that produces non-compiling code fails the test.

Comments below, roughly in priority order.

1. BRT007/BRT008 silently miss the post-construction assignment they warn about elsewhere

KafkaPublicationPartitionerAnalyzer.cs:93-116

if (!visitor.IsPartitionerAssigned)
{
    if (IsPartitionerAssignedAfterConstruction(operation)) return;   // <-- bails out entirely
    report BRT006;
}
else if (visitor.IsConsistentRandom) { ... }

IsPartitionerAssignedAfterConstruction only answers whether an assignment exists — it never looks at the value. So:

var publication = new KafkaPublication();
publication.Partitioner = Partitioner.Consistent;   // no diagnostic at all

produces no warning, while the equivalent initializer form produces BRT008. That's an inconsistency users will notice, and it's an easy accidental suppression: assign the discouraged value on the next line and the rule goes quiet. Worse, it's exactly the shape a user lands on after "fixing" BRT006 by setting the partitioner explicitly.

Suggestion: have the helper return the matching ISimpleAssignmentOperation (or null) instead of bool, then run the same GetPartitionerValueName classification over its value and report BRT007/BRT008 at that assignment's location. PartitionerValueCodeFixProvider would need a small tweak — its FindNode(...).OfType<AssignmentExpressionSyntax>() lookup already handles a bare Partitioner = ... assignment, but the a.Left is IdentifierNameSyntax filter won't match publication.Partitioner, so it'd need to accept a MemberAccessExpressionSyntax on the left too.

A test for the discouraged-value-after-construction case would pin this down.

2. The analyzer allocates a visitor for every new in the compilation

KafkaPublicationPartitionerAnalyzer.cs:81-91

RegisterOperationAction(..., OperationKind.ObjectCreation) fires for every object creation in every file, and the first thing it does is new KafkaPublicationPartitionerVisitor() followed by a symbol-visitor walk up the base-type chain. In the IDE this runs on every keystroke, in solutions where the overwhelming majority of new expressions have nothing to do with Kafka — and in solutions that don't reference the Kafka gateway at all.

Two cheap improvements:

  • Wrap registration in RegisterCompilationStartAction and bail immediately if compilation.GetTypeByMetadataName("Paramore.Brighter.MessagingGateway.Kafka.KafkaPublication") is null. Solutions without the Kafka gateway then pay nothing.
  • Do the type check in the analyzer against the resolved symbol before allocating the visitor, so the visitor is only constructed for actual KafkaPublication creations.

Comparing against a GetTypeByMetadataName symbol would also be stronger than ChildOfVisitor's symbol.Name == "KafkaPublication" && assembly.Name == "..." string match — though I recognise that visitor is pre-existing and shared, so changing it is out of scope for this PR.

Also: operation.Type! at KafkaPublicationPartitionerVisitor.cs:45 will throw if Type is null, which can happen in the IDE against partially-typed or erroneous code. AD0001 is in the repo's NoWarn, so an analyzer crash would be invisible in the build. A null check costs nothing.

3. The code fix uses the assembly-name constant as a namespace

MissingPartitionerCodeFixProvider.cs:84

SyntaxFactory.ParseExpression($"{BrighterAnalyzerGlobals.KafkaMessagingGatewayAssembly}.{BrighterAnalyzerGlobals.PartitionerEnum}")

KafkaMessagingGatewayAssembly is documented (and used in ChildOfVisitor) as an assembly name. It happens to equal the namespace today, so the generated Paramore.Brighter.MessagingGateway.Kafka.Partitioner is correct — but the two concepts are conflated, and if the assembly is ever renamed or the enum moved, the code fix silently emits a wrong qualifier while the analyzer keeps matching. Worth a separate KafkaNamespace constant.

Relatedly, PartitionerProperty and PartitionerEnum are two constants with the identical value "Partitioner". That's defensible since they mean different things, but a comment saying so would help.

4. Test coverage gaps

The happy paths are covered well. Missing cases that map to code paths that actually exist:

  • objectCreation.Initializer.AddExpressions(assignment) (MissingPartitionerCodeFixProvider.cs:92) is untested — both fix tests hit the Initializer == null branch. Appending to an existing multi-property initializer is the common real-world case, and it's also where the Formatter.Annotation round-trip is most likely to produce surprising output. Please add a test with e.g. new KafkaPublication { Topic = new RoutingKey("x"), NumPartitions = 3 }.
  • The bare-identifier branch at PartitionerValueCodeFixProvider.cs:95 (_ => newName, for using static ...Partitioner) is untested.
  • The "no fix offered" guard at PartitionerValueCodeFixProvider.cs:65-72 is untested. Note it's only reachable for shapes like Partitioner = (Partitioner.Consistent) — a parenthesised member access — since GetPartitionerValueName already requires an IFieldReferenceOperation. Either add a test or simplify the guard if you conclude it's unreachable in practice.
  • Negative test for non-Kafka publications — nothing asserts BRT006 stays quiet for a plain Publication or an RmqPublication. ChildOfVisitor's assembly check makes this correct today, but it's exactly the kind of thing a future refactor of that shared visitor would break.
  • User subclass of KafkaPublicationChildOfVisitor walks base types, so class MyPublication : KafkaPublication will be flagged. That looks intended; a test would lock it in.
  • BRT007/BRT008 after construction — see point 1.

The analyzer tests also repeat testContext.TestState.AdditionalReferences.Add(...KafkaPublication...) in all nine tests. BaseCodeFixTest already does this in its constructor; consider a BaseKafkaAnalyzerTest (or adding it to BaseAnalyzerTest) to remove the duplication.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer and code fixes (2/2)

5. New rules were added to AnalyzerReleases.Shipped.md, not Unshipped.md

BRT006–BRT008 were appended to the existing ## Release 1.0 section of AnalyzerReleases.Shipped.md, and AnalyzerReleases.Unshipped.md is left empty. The Roslyn convention is that new rules go into Unshipped.md and are moved into a new Shipped.md release section at release time — retroactively editing a published release's rule table misrepresents what 1.0 contained. If 1.0 hasn't actually shipped yet this is harmless; if it has, please move these three rows to Unshipped.md.

6. Severity of BRT006 — flagging for a maintainer decision, not asking for a change

BRT006 at Warning will fire on essentially every existing KafkaPublication in every consumer codebase, because KafkaPublication.Partitioner has a default (ConsistentRandom, src/Paramore.Brighter.MessagingGateway.Kafka/KafkaPublication.cs:104) and almost nobody sets it explicitly today. For any consumer with TreatWarningsAsErrors — which this repo itself uses in src/ — upgrading the analyzer package becomes a build break across their whole Kafka configuration.

BRT006.md argues the case for Warning deliberately and well, so this is a judgement call rather than a defect. But it's worth an explicit maintainer sign-off and a line in the release notes given the upgrade impact. DiagnosticSeverity.Info would surface the suggestion in the IDE without breaking builds, if the team wants a softer landing.

Related tension worth noting: the library's own default is the value BRT007 discourages. Nothing to change here (per the repo's "don't change defaults" guidance), but if the team ever intends to move that default to Murmur2Random, sequencing it against BRT006/BRT007 matters.

I checked, and no project in this repo consumes the analyzer package, so there's no in-repo warning fallout from merging.

7. Smaller notes

  • Diagnostic message length (KafkaPublicationPartitionerAnalyzer.cs:53-54, 64-65): the BRT007/BRT008 messageFormat strings are two full sentences. Squiggle tooltips and build logs get noisy fast. Convention is a short messageFormat with the rationale in the (currently unset) description parameter — the "Existing publications can keep..." sentence in particular belongs there.
  • IsPartitionerAssignedAfterConstruction loop shape (:138-152): the for loop walks ancestors but returns unconditionally inside the first if (ancestor is IBlockOperation), so it only ever examines the nearest enclosing block. The loop shape implies it keeps searching outward. Either simplify it to a "find nearest enclosing block" expression, or actually continue outward — the latter would fix the false positive where the declaration and the assignment sit in different blocks:
    var publication = new KafkaPublication();
    if (flag) { publication.Partitioner = Partitioner.Murmur2Random; }  // BRT006 still fires
    Field targets (_publication = new KafkaPublication(); _publication.Partitioner = ...;) also false-positive. Both are called out as limitations in BRT006.md, which is the right call for a first cut — just make sure that suppression guidance is discoverable.
  • RegisterCodeFixesAsync inconsistency: MissingPartitionerCodeFixProvider handles only context.Diagnostics[0] while PartitionerValueCodeFixProvider loops over context.Diagnostics. Harmless in practice, but pick one for consistency.
  • Nested-publication edge case (KafkaPublicationPartitionerVisitor.cs:43-57): the comment says descent happens "only when this operation is the KafkaPublication itself", but base.VisitObjectCreation walks all children, so a nested KafkaPublication carrying a Partitioner inside an outer one's initializer would mark the outer as assigned. Contrived enough that I wouldn't fix it, but the comment overstates the guarantee.
  • Unrelated structural changes bundled in: BrighterAnalyzerGlobals.cs and DiagnosticsIds.cs were converted to file-scoped namespaces, and the BOM was stripped from BrighterAnalyzerGlobals.cs, in the same commit as the behavioural change. Per CLAUDE.md's tidy-first guidance these belong in a separate structural commit — as it stands they make the behavioural diff harder to read than it needs to be.
  • License header attribution: KafkaPublicationPartitionerAnalyzer.cs and MissingPartitionerCodeFixProvider.cs are © Ian Cooper while the sibling KafkaPublicationPartitionerVisitor.cs is © Aboubakr Nasef, and the PR author is neither. Worth a quick check that this matches what the project intends. The new test files carry no header, which is consistent with the existing analyzer tests.
  • Packaging looks right to me: both DLLs land in analyzers/dotnet/cs, SuppressDependenciesWhenPacking keeps the Microsoft.CodeAnalysis.CSharp.Workspaces reference out of the nuspec, and the CodeFixes project correctly omits EnforceExtendedAnalyzerRules (which would trip RS1038 on the Workspaces reference). One thing I couldn't verify without running a pack: whether shipping the CodeFixes DLL alongside the analyzer produces a CS8032 analyzer-load warning under command-line csc, where Workspaces isn't present. The official Roslyn template does the same thing, so it's probably fine — but a manual dotnet pack plus consume-the-nupkg smoke test would confirm it, since the test project references the projects directly and wouldn't catch it.

Not applicable

No security concerns — this is build-time tooling with no runtime, network, or user-input surface.


Nothing here is a hard blocker except possibly item 1, which I'd want fixed (or consciously deferred with a tracking issue) because it makes the rule set self-inconsistent in a way that's easy to hit by accident. Items 2, 4 and 5 are worth doing in this PR; the rest are judgement calls or follow-ups. Nice work — the docs in particular set a good bar for the other BRT rules to follow.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer (BRT006-BRT008)

Nice piece of work overall. Worth calling out what is genuinely good before the feedback:

  • The RegisterCompilationStartAction gate on GetTypeByMetadataName(...KafkaPublication) means solutions that do not reference the Kafka gateway pay nothing - the right shape for an analyzer, and better than the existing BRT001-BRT005 analyzers, which register unconditionally.
  • The cheap IsKafkaPublicationType check before allocating the visitor in AnalyzerObjectCreation is the correct ordering.
  • s_kafkaPublicationCheck is a stateless ChildOfVisitor, so sharing it statically is safe under EnableConcurrentExecution().
  • helpLinkUri on all three descriptors - the existing rules do not have this. Worth backfilling BRT001-BRT005 in a follow-up.
  • The docs (BRT006-BRT008) are genuinely good: they explain why, and they are honest that applying the fix re-partitions the topic. That honesty is the right call.
  • The code-fix tests use CompilerDiagnostics.Errors, so the fixed code is verified to compile.

Findings below, roughly in severity order. (Reviewed statically - this run could not execute dotnet build/dotnet test, so I have not run the new suite.)


1. PartitionerValueCodeFixProvider can rewrite the wrong assignment

src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/PartitionerValueCodeFixProvider.cs:59-66

BRT007/BRT008 are reported at the location of the whole object creation (KafkaPublicationPartitionerAnalyzer.cs:125,131), so the fix provider has to go hunting for the assignment inside it. It does that with a purely syntactic FirstOrDefault over all descendants, matching on the identifier text Partitioner, with no check that the assignment actually belongs to the diagnosed KafkaPublication.

Your own analyzer test When_Nested_Object_Has_Own_Partitioner_Property_Should_Still_Report_Missing_Partitioner shows the exact shape that breaks this. Combine it with a discouraged value on the outer publication:

class Config { public Partitioner Partitioner { get; set; } }
...
var fallback = Partitioner.Random;
var publication = new KafkaPublication
{
    DefaultHeaders = new Dictionary<string, object>
    {
        ["cfg"] = new Config { Partitioner = fallback }   // matched first, Right is an IdentifierName
    },
    Partitioner = Partitioner.Consistent                  // the one BRT008 actually fired on
};

In document order the nested Partitioner = fallback is found first, its Right is an IdentifierNameSyntax so it passes the guard on lines 68-70, and the fix rewrites Config.Partitioner to Murmur2 - leaving the real BRT008 unfixed and producing an edit the user did not ask for.

The clean fix is to report the diagnostic on the assignment (or the value) rather than on the object creation. That kills this class of bug outright, and it is also better UX: right now a value-preference warning squiggles the entire multi-line new KafkaPublication { ... } expression. KafkaPublicationPartitionerVisitor already visits the assignment, so it could capture operation.Syntax.GetLocation() alongside IsConsistent/IsConsistentRandom, and the code fix would then use FindNode directly with no searching at all. It would also make the two paths consistent: AnalyzeAssignment already reports at the assignment, so today the same rule reports at two different granularities depending on where the value was set.

If you would rather keep the object-creation location, at minimum restrict the search to objectCreation.Initializer.Expressions (direct children only) instead of DescendantNodesAndSelf(), and confirm the property containing type via the semantic model.

2. MissingPartitionerCodeFixProvider relocates trailing comments

src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/MissingPartitionerCodeFixProvider.cs:113-137

AddInitializerExpression moves the last expression entire trailing trivia onto the new expression. Trailing trivia includes trailing comments, so:

var p = new KafkaPublication
{
    Topic = new RoutingKey("x"),
    NumPartitions = 3 // one per shard
};

becomes

var p = new KafkaPublication
{
    Topic = new RoutingKey("x"),
    NumPartitions = 3,
    Partitioner = Partitioner.Murmur2Random // one per shard
};

The comment now documents the wrong member. Either filter the moved trivia to whitespace/newlines only, or - probably simpler - drop the manual trivia rewiring, give the new expression SyntaxFactory.ElasticCarriageReturnLineFeed as leading trivia, and let the Formatter.Annotation pass you already run place the comma and indentation. Worth checking whether the formatter alone resolves the misplaced-comma problem the comment on lines 115-121 describes; if it does, the whole helper can go.

Related: because Formatter.Annotation is attached to the whole newObjectCreation, applying the fix reformats every existing member of the initializer, not just the added line. That shows up as diff noise on hand-aligned initializers.

3. Behaviour-changing fix exposed through Fix All

Both providers return WellKnownFixAllProviders.BatchFixer. As BRT006.md correctly states, applying the fix re-partitions the topic - keys map to different partitions than before. With Fix All to Solution, one click silently changes the message routing of every Kafka publication in a codebase, and the code action title (Set Partitioner to Partitioner.Murmur2Random) gives no hint of that.

Worth a maintainer decision, but my suggestion: return null from GetFixAllProvider() for MissingPartitionerCodeFixProvider (and arguably for BRT007/BRT008 too), so the change stays a deliberate per-site choice.

Same theme for severity: BRT006 fires on every KafkaPublication that does not set Partitioner, including code that is perfectly correct and intentionally taking the default. For an existing user turning the package on, that is a wall of warnings - and this repo src/Directory.Build.props sets TreatWarningsAsErrors=true, so anyone with the same setting gets a build break rather than a nudge. BRT006.md argues for Warning parity with BRT007, which is defensible; I would just want that to be an explicit maintainer call. DiagnosticSeverity.Info is the more conventional choice for "make this explicit" rules.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

(review continued from the previous comment)

4. Release tracking: new rules went into AnalyzerReleases.Shipped.md

src/Paramore.Brighter.Analyzer/AnalyzerReleases.Shipped.md:9-11 - BRT006-BRT008 were appended to the existing ## Release 1.0 section, while AnalyzerReleases.Unshipped.md is left empty. The convention these files exist to support (RS2000/RS2001) is that new rules land in Unshipped.md and get promoted into Shipped.md at release time; putting them under Release 1.0 retroactively claims they were in a version that shipped without them.

Nothing enforces it today (Microsoft.CodeAnalysis.Analyzers is not referenced even though EnforceExtendedAnalyzerRules is set on the analyzer project), so this is a convention point - but if the package has been published, please move these three rows to Unshipped.md.

5. Unrelated reformatting mixed into a feature PR

BrighterAnalyzerGlobals.cs and DiagnosticsIds.cs were converted to file-scoped namespaces (and the BOM was stripped from BrighterAnalyzerGlobals.cs) in the same change as the new behaviour. The Change Scope rule in CLAUDE.md and the /tidy-first guidance both ask for structural and behavioural changes to be separated - the reformat re-indents ~30 lines and makes the genuinely new constants harder to spot in the diff. Ideally a separate tidying commit; not a blocker.

6. Test coverage gaps

The suite is solid on the happy paths. Cases I would want added:

  • Target-typed new: KafkaPublication p = new();. Use of BaseObjectCreationExpressionSyntax in the fix provider suggests this is intended to work, but nothing verifies the analyzer or the fix against ImplicitObjectCreationExpressionSyntax.
  • Assignment to a field/property rather than a local: _publication = new KafkaPublication(); _publication.Partitioner = ...; - FindPartitionerAssignmentAfterConstruction (KafkaPublicationPartitionerAnalyzer.cs:186-192) only handles ILocalSymbol, so BRT006 fires spuriously here. BRT006.md documents the helper-method limitation but not this one. A test pinning the behaviour plus a docs line would be good.
  • Assignment textually before the construction: the block scan ignores ordering, so p.Partitioner = ...; p = new KafkaPublication(); suppresses BRT006 on the second construction. Minor false negative, worth pinning.
  • KafkaPublication<T> with a discouraged value - the generic case is only covered for BRT006.
  • No Fix All test for either provider, which matters given point 3.
  • No test for the comment-relocation case in point 2.

7. Analyzer test snippets do not compile

BaseAnalyzerTest sets CompilerDiagnostics.None, so the new snippets are never checked for validity - and at least one is not valid. In When_KafkaPublication_Generic_Is_Created_Without_Partitioner_Should_Report_Missing_Partitioner:

class MyRequest : IRequest
{
    public System.Guid Id { get; set; }
    public System.Guid SpanId { get; set; }
}

IRequest requires Id Id { get; set; } (Paramore.Brighter.Id, not Guid) and Id? CorrelationId { get; set; }, and KafkaPublication<T> is constrained where T : class, IRequest. So this snippet has CS0535/CS0311 errors that are being swallowed. It happens not to change the outcome here, but analyzers behave differently against error-recovery symbols, and a test whose input does not compile is not testing what it claims. Consider CompilerDiagnostics.Errors for the new Kafka analyzer tests, as you already do in BaseCodeFixTest.

8. Smaller items

  • KafkaPublicationPartitionerVisitor.IsKafkaPublication (line 37) is set but never read - the analyzer uses the static IsKafkaPublicationType instead. Dead public surface; drop it.
  • FindPartitionerAssignmentAfterConstruction returns ISimpleAssignmentOperation but the only caller does != null (KafkaPublicationPartitionerAnalyzer.cs:113). Return bool and rename to HasPartitionerAssignmentAfterConstruction.
  • MissingPartitionerRule is the only one of the three descriptors without a description:. The other two carry the "existing publications can keep their value" caveat, which is exactly the nuance BRT006 also needs in the IDE tooltip.
  • PartitionerValueCodeFixProvider bails on anything that is not a MemberAccessExpressionSyntax/IdentifierNameSyntax (lines 68-70). The parenthesized case (Partitioner.Consistent), which you have a test for, would be handled by a single unwrap of ParenthesizedExpressionSyntax. Low value, but cheap.
  • The fix title is Use Partitioner.{target} even in the using static path, where the emitted code is a bare Murmur2Random. Cosmetic.
  • Paramore.Brighter.Analyzer.CodeFixes.csproj does not set EnforceExtendedAnalyzerRules while the analyzer project does. That is arguably correct (RS1038 would fire on the Microsoft.CodeAnalysis.CSharp.Workspaces reference), but a one-line comment saying so would save the next reader the investigation.
  • Packaging: shipping Paramore.Brighter.Analyzer.CodeFixes.dll into analyzers/dotnet/cs is the standard layout (StyleCop.Analyzers does the same), but please pack the .nupkg and do one command-line dotnet build against it to confirm no CS8032/AD0001 from csc probing an assembly whose Workspaces dependency is not present outside the IDE.
  • PR checklist: "My changes follow the existing code style and conventions" and the Contributing Guide box are unticked - worth confirming.

Summary: the analyzer itself is well-built and the docs are a highlight. Item 1 is the one I would treat as blocking (the fix can edit the wrong node and leave the reported diagnostic unfixed), and moving the BRT007/BRT008 location onto the assignment resolves it while simplifying both the fix provider and the diagnostic UX. Items 2 and 3 are quality issues on the code-fix side; 4-8 are convention, coverage, and polish.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer (BRT006–BRT008)

Solid work. Several things here are better than the existing analyzers and worth keeping as the house pattern: the RegisterCompilationStartAction guard that bails when KafkaPublication isn't in the compilation (BRT001 pays for an operation callback in every solution; this one doesn't), helpLinkUri + description on the descriptors, comments that explain why each limitation exists and match them to the docs, and broad analyzer test coverage — generics, subclasses, target-typed new, nested creations, post-construction assignment, before/after ordering, and a decoy Partitioner property on an unrelated type.

I have not relitigated the two decisions recorded in the description (BRT006 at Warning, Fix All available). §1 below is a distinct problem, not the severity argument again.


1. BRT006 false-positives on subclasses that set Partitioner in their own constructor — and the fix then overrides that choice

AnalyzerObjectCreation only inspects the initializer at the construction site:

class OrdersPublication : KafkaPublication
{
    public OrdersPublication() { Partitioner = Partitioner.Murmur2Random; }
}

var p = new OrdersPublication();   // BRT006: "Partitioner assignment is missing"

The warning is wrong, and the offered fix rewrites this to new OrdersPublication { Partitioner = Partitioner.Murmur2Random } — for a subclass that deliberately chose a different partitioner, that silently overrides the decision at every construction site, and Fix All lands it solution-wide in one action.

Not hypothetical: KafkaPublication<T> in this repo (src/Paramore.Brighter.MessagingGateway.Kafka/KafkaPublication.cs:163) sets an inherited property from its constructor. When_KafkaPublication_Subclass_Is_Created_Without_Partitioner_Should_Report_Missing_Partitioner pins the behaviour for an empty subclass, which makes the gap easy to miss.

Suggested: when operation.Type isn't exactly KafkaPublication/KafkaPublication<T>, check whether the declared type (or any base up to KafkaPublication) assigns Partitioner in an instance constructor or property initializer, and skip BRT006 if so. If that's more than you want here, suppress BRT006 for derived types entirely and note it in BRT006.md — a false negative on a subclass is far cheaper than a Fix All that rewrites intent.

2. GetPartitionerValueName matches on field name only, so the code fix can emit code that doesn't compile

KafkaPublicationPartitionerVisitor.cs:98:

return value is IFieldReferenceOperation fieldReference ? fieldReference.Field.Name : null;

Everywhere else the analyzer verifies the containing type; here it doesn't. So:

static class Defaults { public static readonly Partitioner Consistent = Partitioner.Murmur2; }

new KafkaPublication { Partitioner = Defaults.Consistent }

reports BRT008 (false positive), and PartitionerValueCodeFixProvider passes CanRewrite (it's a MemberAccessExpressionSyntax) and rewrites the value to Defaults.Murmur2 — which doesn't exist. The fix produces broken code. Same shape for a bare identifier field named Consistent in scope.

One line fixes it: check fieldReference.Field.ContainingType is the Kafka Partitioner enum (name + KafkaMessagingGatewayAssembly, the same test IsKafkaPublicationType already does) before returning the name.

3. BRT008.md's rationale is factually inverted for Murmur2

Murmur2's more uniform distribution keeps load balanced and avoids this, and it also matches the partitioning the standard Kafka clients use by default

KafkaMessageProducer.cs:133 casts Brighter's enum straight onto Confluent.Kafka.Partitioner, so librdkafka's semantics apply:

value behaviour
consistent CRC32 of key; empty/NULL keys → a single partition
consistent_random CRC32 of key; empty/NULL keys randomly partitioned
murmur2 Java-compatible Murmur2; NULL keys → a single partition
murmur2_random Java-compatible Murmur2; NULL keys randomly partitioned — "functionally equivalent to the default partitioner in the Java Producer"
  • The Java-compatibility claim is on the wrong value. murmur2_random, not murmur2, is what the standard Java producer does. BRT008.md asserts the opposite.
  • Murmur2 reintroduces the exact hot-partition failure mode the doc warns about, funnelling every keyless message onto one partition. If hot partitions are the driver, BRT008 should recommend Murmur2Random too (matching BRT006/BRT007), with Murmur2 offered only as the strict like-for-like swap for callers who deliberately want NULL-keyed messages pinned.

Separately, the premise shared by BRT007 and BRT008 — that CRC32 "spreads keys less evenly" than Murmur2 — isn't something librdkafka or the Kafka docs claim. The documented differentiator between the consistent* and murmur2* families is cross-client key compatibility with the Java producer, not hash quality. These strings ship in the IDE and justify a change that re-partitions a live topic, so I'd lead with the interop argument and soften or drop the distribution-quality one.

4. Test coverage gaps, all on the code-fix side

  • No Fix All test. Both providers override GetFixAllProvider(), the description discusses Fix All explicitly, and AddInitializerExpression does manual trivia surgery — yet no test puts two KafkaPublications in one document. CSharpCodeFixTest supports this via NumberOfFixAllIterations / BatchFixedCode.
  • No single-line-initializer test. AddInitializerExpression derives endOfLineTrivia with SkipWhile(t => !t.IsKind(EndOfLineTrivia)); for new KafkaPublication { Topic = x } that's empty and commentTrivia becomes trailing whitespace, which then gets moved onto the separator. The formatter probably rescues it, but only multi-line initializers are exercised.
  • No "no fix offered" test for the CanRewrite bail-out (e.g. Partitioner = (Partitioner)Partitioner.Consistent) — pin it with FixedCode = TestCode.
  • No tests for the false positives in §1 and §2.

5. Performance

RegisterCompilationStartAction already resolves the KafkaPublication symbol and discards it. IsKafkaPublicationType then walks the base chain doing two string comparisons per level for every ObjectCreation and every Partitioner-named SimpleAssignment in the compilation. Capture the INamedTypeSymbol in the compilation-start closure and use SymbolEqualityComparer.Default instead — same for the Partitioner enum symbol that §2 needs anyway. It also drops the assembly-name string dependency, so the check survives a rename.

6. Smaller things

  • The IMemberInitializerOperation comment is wrong (KafkaPublicationPartitionerAnalyzer.cs:148-149): it isn't about with expressions, it represents a nested member initializer — new Holder { Publication = { Partitioner = ... } }. The guard is harmless, but that form is a silent false negative: neither AnalyzerObjectCreation (no new KafkaPublication) nor AnalyzeAssignment (skipped by the guard) fires. Handle it or list it with the other documented limitations.
  • HasPartitionerAssignmentAfterConstruction compares symbols, not instances for field/property targets, so a.Pub = new KafkaPublication(); b.Pub.Partitioner = ...; suppresses BRT006 on a. Minor; a comment would do.
  • AnalyzerReleases: BRT006–BRT008 were added to Shipped.md under the existing ## Release 1.0 heading while Unshipped.md sits empty. Convention is that new rules land in Unshipped and the release process promotes them; editing a shipped section retroactively rewrites what 1.0 contained.
  • Licence headers: KafkaPublicationPartitionerVisitor.cs credits Aboubakr Nasef, the analyzer and both code fixes credit Ian Cooper, on a PR authored by @lillo42 — looks like template copy-paste.
  • XML docs are absent on the new public types/members. Consistent with the existing analyzer files, so not a regression, but .agent_instructions/documentation.md asks for docs on exports and the checklist item is unticked — flagging so it's a conscious skip.
  • MissingPartitionerCodeFixProvider.cs:67: var target = ...Murmur2RandomPartitionerValue; is a local that's never anything else — inline it, or drop the parameter from AddPartitionerAsync.
  • New files use file-scoped namespaces while the surrounding analyzer files use block-scoped. Not wrong, just inconsistent within the folder.

Summary. §1 and §2 are what I'd want addressed before merge — both can make a code fix silently write incorrect code, the worst failure mode for an analyzer shipping Fix All. §3 is a defect in the guidance rather than the code, but that guidance is what justifies re-partitioning a production topic. §4–§6 are follow-ups.

Reviewed statically — dotnet build/dotnet test weren't runnable in this environment, so I have not executed the suite.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer (BRT006–BRT008)

Overall this is a well-built analyzer. The things that usually go wrong in Roslyn analyzers are handled correctly here: symbols are resolved once in RegisterCompilationStartAction with an early-out when the Kafka gateway isn't referenced, ConfigureGeneratedCodeAnalysis/EnableConcurrentExecution are set, there's no Compilation.GetSemanticModel call (RS1030), GetPartitionerValueName correctly guards against a user field that merely shares a member name, and the double-reporting interaction between AnalyzerObjectCreation and AnalyzeAssignment is thought through (the IMemberInitializerOperation carve-out is a nice catch). The BRT006–008 docs are genuinely good — better than most analyzer docs — and adding helpLinkUri is an improvement over BRT001–005, which have none.

I couldn't build in this environment, so the notes below are from static reading.

I'm treating the two "Maintainer decisions" in the PR description as settled and not relitigating them.


Bugs

1. MissingPartitionerCodeFixProvider throws on an empty object initializerMissingPartitionerCodeFixProvider.cs:121

var lastExpression = initializer.Expressions.Last();

new KafkaPublication { } is legal C#, and the analyzer does report BRT006 for it (the visitor finds no Partitioner assignment). But objectCreation.Initializer is non-null, so AddPartitionerAsync takes the AddInitializerExpression path and Expressions.Last() throws on the empty list. The IDE will offer the fix and then fail when it's previewed or applied; under Fix All it takes the whole batch down.

if (initializer.Expressions.Count == 0)
{
    return initializer.WithExpressions(
        SyntaxFactory.SingletonSeparatedList(expression));
}

2. A trailing comma sends a multi-line initializer down the single-line branchMissingPartitionerCodeFixProvider.cs:124-129

The branch is chosen by whether the last expression's trailing trivia contains an EndOfLineTrivia. With a trailing comma:

new KafkaPublication
{
    Topic = new RoutingKey("x"),
    NumPartitions = 3,
}

the newline hangs off the trailing , token, not off NumPartitions = 3, so trailingTrivia is empty, fromEndOfLine.Count == 0, and the fix takes the "keep it on one line, single spaces" path for what is plainly a multi-line initializer. The new expression gets a single leading space while the existing separator already carries the newline. Formatter.FormatAsync may rescue the indentation, but at minimum the trailing-comma style is lost (which dotnet format/editorconfig setups will churn back), and I'd want a test to know what actually comes out. Trailing commas are a very common style, so this is worth covering explicitly:

  • { A = 1, B = 2, } (multi-line, trailing comma)
  • { A = 1, } (single expression, trailing comma)

3. flake.nix looks unrelated and doesn't work

It's not mentioned in the PR description and has nothing to do with the analyzer. It's also broken as written: flake-utils is destructured in the outputs argument but never declared in inputs, so nix develop fails to evaluate. And DOTNET_ROOT is a top-level flake output attribute, not part of the devShell's environment, so it has no effect either. Suggest dropping it from this PR — it can land separately (with flake-utils added to inputs and DOTNET_ROOT moved into mkShell), or live in a gitignored personal file.


Correctness / accuracy

4. The sample comment is wrong about the client default — repeated in all 10 sample edits

// ... and matches the standard Kafka client default

librdkafka (and therefore Confluent.Kafka) defaults partitioner to consistent_random — which is exactly what KafkaPublication.Partitioner defaults to today (KafkaPublication.cs:104). Murmur2Random matches the Java producer's default, not the client Brighter actually uses. BRT007.md states this correctly ("the algorithm the standard Java Kafka producer uses"); the sample comment should match. As written it tells readers the opposite of what BRT006.md says about the implicit default.

Separately: the same two-line comment is copy-pasted into 10 sample files. One explanation in one canonical sample (KafkaTaskQueue) plus a link to BRT007.md would carry the same information with less to keep in sync.

5. The BRT007 rule description doesn't match BRT007.md

The descriptor says:

'Murmur2Random' spreads keys more evenly across partitions than the CRC32-based 'ConsistentRandom', avoiding hot partitions.

but BRT007.md correctly identifies the real rationale as cross-client hash compatibility (both variants already spread keyless messages randomly; the hot-partition argument is BRT008's). The descriptor text is what shows in the IDE tooltip, so it's the version most users will actually read. Worth aligning it with the doc.

6. Five sample files lost their UTF-8 BOMKafkaDynamicEventStream, KafkaSchemaRegistry, KafkaTaskQueue, KafkaTaskQueueWithDLQ, MultiBus, ConfigureTransport.cs

The diff shows the leading BOM stripped from the first line of each. Harmless but unrelated churn that shows up in the diff; probably an editor setting.


Release tracking

7. New rules were added to AnalyzerReleases.Shipped.md under "Release 1.0"; AnalyzerReleases.Unshipped.md is empty

The Roslyn release-tracking convention (RS2000/RS2001) is that new rules go into Unshipped.md and are moved to Shipped.md under a new version heading when that version is cut. As written, the file asserts BRT006–008 were part of release 1.0, which isn't true. Moving them to Unshipped.md keeps the history honest and keeps the tracking analyzers happy if they're ever enabled.


Performance / analyzer hygiene

8. SetsPartitionerInConstructor re-walks constructor syntax at every construction site, and drops the cancellation tokenKafkaPublicationPartitionerAnalyzer.cs:159-206

syntaxReference.GetSyntax() is called without context.CancellationToken (there's an overload that takes one) — in the IDE this means a keystroke-triggered analysis pass can't be cancelled mid-walk. And for a codebase with one OrdersPublication subclass instantiated in 50 places, the same constructor bodies get parsed and walked 50 times. Both are cheap to fix: build a ConcurrentDictionary<INamedTypeSymbol, bool> (with SymbolEqualityComparer.Default) inside RegisterCompilationStartAction and memoise per type, threading the token through.

9. BRT006 squiggles the entire object creationKafkaPublicationPartitionerAnalyzer.cs:133

context.Operation.Syntax.GetLocation() covers new KafkaPublication { ... } in full — in the samples that's 10+ lines underlined. Reporting on the type name instead (or objectCreation.NewKeyword) is the usual convention and reads much better in the IDE; the code fix's root.FindNode(...).DescendantNodesAndSelf().OfType<BaseObjectCreationExpressionSyntax>() walk would need to become an ancestor walk, but nothing else changes.


False negatives worth documenting

10. A subclass from a referenced assembly always trips BRT006 with no fixable remedy

ConstructorAssignsPartitioner only inspects DeclaringSyntaxReferences, so a KafkaPublication subclass that lives in a compiled dependency and sets Partitioner in its constructor has none — BRT006 fires on new TheirPublication() and the only escape is suppression. Same for a subclass that sets it via a property/field initializer or a helper method called from the constructor. BRT006.md:16 already lists the helper-method limitation; adding the cross-assembly case to that sentence would save users a puzzled hour.


Tests

Coverage is solid — ~30 analyzer cases plus 15 code-fix cases, with the tricky ones (target-typed new, generic KafkaPublication<T>, using static, nested member initializers, user field named like the enum member, before-vs-after construction) all present. Gaps that map to the findings above:

  • empty initializer new KafkaPublication { } (finding 1 — this one currently crashes)
  • trailing-comma initializer (finding 2)
  • subclass declared in metadata rather than source (finding 10)
  • no test asserting BRT006 is reported exactly once for the nested-publication case that KafkaPublicationPartitionerVisitor.cs:56-60 calls out as an accepted edge case — worth pinning the current behaviour so a future change to the visitor's descent doesn't silently start double-reporting

Minor: BaseCodeFixTest duplicates the reference/OutputKind setup from BaseAnalyzerTest + BaseKafkaAnalyzerTest verbatim. A small shared helper for "add Brighter + Kafka references" would avoid the three copies drifting.


One suggestion on the maintainer decisions

Not reopening either decision — but since both fixes change runtime behaviour, putting that consequence in the code-action title costs nothing and is the one place the IDE guarantees the user sees it before clicking (the rule description isn't shown in the lightbulb):

title: $"Set 'Partitioner' to 'Murmur2Random' (re-partitions the topic)"

Nice work overall — the analyzer logic and the docs are both above the bar. Finding 1 is the only one I'd call blocking.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer + code fixes

Overall this is high-quality analyzer work. The things I'd normally flag are already handled: symbol resolution is hoisted into RegisterCompilationStartAction and bails out when the Kafka gateway isn't referenced, GeneratedCodeAnalysisFlags.None + EnableConcurrentExecution are set, the enum member is matched by ContainingType symbol rather than by name (so Defaults.Consistent isn't mistaken for the enum), and the code fix declines to rewrite values it can't safely rewrite. The trickier shapes — target-typed new(), generics, subclasses, nested member initializers, trailing commas, trailing comments, using static — all have tests. Nice.

A few things I think should be addressed before merge, then some smaller notes.


1. The packaged .editorconfig files were not updated (blocking, I think)

src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.props wires two global configs — configuration/none.editorconfig when BrighterAnalysisMode is None, and configuration/default.editorconfig when it is Default.

Both files still list only BRT001–BRT005. So:

  • A consumer who sets BrighterAnalysisMode=None to turn Brighter rules off still gets BRT006/007/008. Given BRT006 is Warning-by-default and fires on every KafkaPublication, that's exactly the escape hatch that needs to work, and it silently doesn't.
  • default.editorconfig is likewise incomplete.

Please add the three new IDs to both files.

(Unrelated and pre-existing, so out of scope here, but worth a follow-up issue: neither file contains is_global = true, which GlobalAnalyzerConfigFiles requires. Worth verifying the analysis-mode switch works end-to-end while you're in here, since this PR is the first change to depend on it mattering.)

2. New rules should go in AnalyzerReleases.Unshipped.md

BRT006–008 were appended to AnalyzerReleases.Shipped.md under ## Release 1.0, which claims they shipped in a release that already went out. AnalyzerReleases.Unshipped.md exists in the project and is empty — that's where new rules belong until they're released, at which point they move down into a new ## Release section.

3. ConstructorAssignsPartitioner over-scans for primary-constructor subclasses

SetsPartitionerInConstructorConstructorAssignsPartitioner walks constructor.DeclaringSyntaxReferences and calls .DescendantNodes(). For an ordinary ConstructorDeclarationSyntax that's exactly the ctor body, which is what the comment describes. But for a primary constructor the declaring syntax is the type declaration, so DescendantNodes() walks the entire class body:

class OrdersPublication(string topic) : KafkaPublication
{
    public void Reconfigure() => Partitioner = Partitioner.Consistent;   // not a ctor assignment
}

var p = new OrdersPublication("orders");   // BRT006 suppressed

Any Partitioner = … anywhere in such a class suppresses BRT006 at every instantiation site. Cheap guard: only accept ConstructorDeclarationSyntax (scanning Body/ExpressionBody rather than the whole node), and treat a TypeDeclarationSyntax reference as "not inspectable" — the same conservative stance you already take for subclasses from referenced assemblies.

4. Nested-block assignment produces two contradictory diagnostics

HasPartitionerAssignmentAfterConstruction only scans block.Operations (direct statements), which BRT006.md documents as a limitation. But the effect is worse than a lone false positive — both rules fire:

var pub = new KafkaPublication();                           // BRT006 (false positive)
if (legacy) { pub.Partitioner = Partitioner.Consistent; }   // BRT008

Applying both fixes yields new KafkaPublication { Partitioner = Murmur2Random } and the Murmur2 assignment in the branch — contradictory code from two "safe" fixes. Switching block.Operations.OfType<IExpressionStatementOperation>() to a block.Descendants() filter looks like it removes both the limitation and the double-report; the SpanStart ordering check still holds.

5. Test coverage: the shape every sample uses isn't tested

Every publication in samples/ is declared inside a collection expression:

[
    new KafkaPublication<GreetingEvent> { Topic =, NumPartitions = 3,}
]

That's the canonical Brighter shape and the exact path AddInitializerExpression's hand-rolled trivia rewiring has to get right (multi-line initializer, deep indentation, element of a collection expression). Neither the analyzer nor the code-fix tests cover it — please add one for each. A test pinning the documented nested-block limitation from #4 would be worth having too, whichever way you resolve it.


Smaller notes

  • BrighterAnalyzerGlobals.KafkaMessagingGatewayAssembly is unused and duplicates KafkaNamespace. Delete it — the comment justifying the duplication only applies to a constant that's actually referenced.
  • The rule description and the doc disagree on the rationale. ConsistentRandomPartitionerRule.description says Murmur2Random "spreads keys more evenly across partitions than the CRC32-based ConsistentRandom, avoiding hot partitions", and the sample comments repeat it 10×. BRT007.md leads with a different and much stronger argument: murmur2 is what the Java producer uses, so a key lands on the same partition across clients. CRC32 is a perfectly uniform hash for this purpose, so the "more evenly / hot partitions" framing is the weaker claim; the descriptor and the sample comments should say what BRT007.md says. (BRT008.md carries the same claim in its second paragraph.)
  • Sample comments: the identical two-line comment pasted into 10 files is a lot of duplication for a point the diagnostic itself already makes. One short clause, or nothing, would read better.
  • Fix output is inconsistent between new KafkaPublication() { Partitioner = … } (single line, When_Partitioner_Is_Missing_Should_Add_Murmur2Random) and the multi-line form for target-typed new() (When_Partitioner_Is_Missing_On_Target_Typed_New_…). Cosmetic, but the tests bake in both shapes.
  • AddInitializerExpression is ~70 lines of manual trivia surgery, and it's the highest-risk code here. Since Formatter.FormatAsync already runs over the annotated node, how much of the whitespace handling is load-bearing versus the comment and trailing-comma handling? Worth trying to shrink it to only what a failing test demands.
  • constructorCheckCache: GetOrAdd reads more simply than TryGetValue + indexer (behaviourally equivalent here; the current form can duplicate work under concurrency, which is harmless).
  • Nullable annotations: KafkaPublicationPartitionerVisitor.PublicationName and PartitionerAssignmentLocation are non-nullable but null until the walk sets them, and GetPartitionerValueName returns null from a string return. The analyzer project doesn't set Nullable=enable, so none of this warns, but the intent would be clearer as string? / Location?.
  • Unrelated reformatting. WrapAttributeAnalyzerTest, SubscriptionConstructorAnalyzerTest, PublicationRequestTypeAssignmentAnalyzerTest and BaseAnalyzerTest are reformatted (file-scoped namespaces, reflowed calls) with no behavioural change — ~109 deletions of noise in a feature PR. CLAUDE.md asks for structural and behavioural changes to be separated (/tidy-first); a separate commit or PR would make both easier to review.

On the two maintainer decisions

Both are reasonable calls and I won't relitigate them. One observation ties back to #1 though: the argument for shipping BRT006 at Warning-by-default rests on each diagnostic being individually suppressible and on consumers being able to opt out — which is precisely what the un-updated none.editorconfig currently prevents. Fixing #1 is what makes that decision defensible in practice.

I wasn't able to build or run the test suite in this environment, so the notes above come from reading the code rather than from a red/green run.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer (BRT006–BRT008)

Reviewed by reading the diff and the surrounding code; I was not able to build or run the test suite in this environment, so everything below comes from static reading.

Overall this is high-quality analyzer work. A few things stand out as done properly rather than approximately:

  • Symbols are resolved once in RegisterCompilationStartAction and the analyzer bails out entirely when the Kafka gateway is not referenced — that is the right shape, and comparisons are by symbol (SymbolEqualityComparer) rather than by name, so Defaults.Consistent or a user type called KafkaPublication will not produce false positives. There is a test for exactly that.
  • SetsPartitionerInConstructor memoises per type, and the syntactic-only constructor check correctly avoids Compilation.GetSemanticModel (RS1030).
  • Test coverage is genuinely strong — 26 analyzer tests and 18 code-fix tests, including the negative cases that usually get skipped: Partitioner.Random, plain Publication, a user field named like an enum member, a cast (diagnostic but deliberately no fix offered), target-typed new, the generic KafkaPublication<T>, nested member initializers, post-construction assignment via local/field/property/parameter, and Fix All.
  • The trivia handling in MissingPartitionerCodeFixProvider.AddInitializerExpression is the part most people get wrong, and each case (trailing comma, trailing comment, single-line vs multi-line, empty braces) has a test pinning it.
  • The packaging changes are a real fix hiding inside a feature PR: build/*.props and configuration/*.editorconfig were never actually packed, and the configs were missing is_global = true, so BrighterAnalysisMode had no effect at all. Renaming the props to Paramore.Brighter.Analyzer.Package.props is required for the NuGet auto-import (it must match the PackageId). Worth calling out in the release notes as a fix in its own right.

The two "maintainer decisions" in the description are yours to make and I am not re-litigating them. Item 2 below is about the docs for that decision, not the decision.


Findings

1. ConstructorAssignsPartitioner can silence BRT006 for a subclass that never sets its own partitionersrc/Paramore.Brighter.Analyzer/Analyzers/KafkaPublicationPartitionerAnalyzer.cs:207

The check is constructor.DeclaringSyntaxReferences then DescendantNodes().OfType<AssignmentExpressionSyntax>(), which sees every assignment anywhere in the constructor, including ones nested inside an unrelated object initializer:

class OrdersPublication : KafkaPublication
{
    public OrdersPublication()
    {
        // configures a *different* publication; OrdersPublication.Partitioner is never set
        _audit = new KafkaPublication { Partitioner = Partitioner.Murmur2 };
    }
}

var p = new OrdersPublication();   // BRT006 suppressed — but nothing set its Partitioner

The comment above the method covers the "local named Partitioner" case as contrived-and-accepted, but this one is a different shape and more plausible. Filtering out assignments that have an InitializerExpressionSyntax / BaseObjectCreationExpressionSyntax ancestor (and arguably a lambda / local-function ancestor) would close it cheaply.

2. The BRT006 remediation is not reachable warning-free, and the doc does not quite say so

The stated purpose of BRT006 is to make the choice visible. But the only way to make the current behaviour visible is Partitioner = Partitioner.ConsistentRandom, which immediately trips BRT007. So a publication that must preserve its existing key-to-partition mapping cannot satisfy BRT006 without also suppressing BRT007 — the warning-free values are Murmur2, Murmur2Random and Random, all of which re-partition.

That is a coherent position, but BRT006.md currently only offers "ignore (or suppress) this warning", which pushes users toward leaving the partitioner implicit — the opposite of what the rule wants. Recommend stating the explicit path directly: "to keep the current assignment while still making the choice visible, set Partitioner = Partitioner.ConsistentRandom and suppress BRT007 for that publication."

3. New rules were added to AnalyzerReleases.Shipped.md, not Unshipped.md

AnalyzerReleases.Unshipped.md exists in the project and was not touched. BRT006–008 have never shipped in a released package, so listing them under the Release 1.0 heading retroactively claims they did. They belong in Unshipped.md until the release that carries them.

4. The BRT007 descriptor text argues something different from BRT007.md

The descriptor (KafkaPublicationPartitionerAnalyzer.cs:61) says Murmur2Random "spreads keys more evenly across partitions than the CRC32-based ConsistentRandom, avoiding hot partitions". BRT007.md makes the correct and much stronger argument: both hash-and-spread comparably, and the real reason to prefer Murmur2Random is that it matches the Java producer partitioner, so a key lands on the same partition across clients. The descriptor is what shows in the IDE tooltip, so it should carry the compatibility argument. (The even-distribution / hot-partition argument is apt for BRT008 vs Consistent, where the null-key pinning and distribution genuinely differ — that doc reads well.)

5. A conditional assignment fully silences BRT006KafkaPublicationPartitionerAnalyzer.cs:284

HasPartitionerAssignmentAfterConstruction searches the whole enclosing block for a later assignment, ordered by SpanStart. So:

var p = new KafkaPublication();
if (useKeys) p.Partitioner = Partitioner.Murmur2;   // BRT006 silenced on every path

The doc advertises nested-block support as a feature, which it is — but the "only on one branch" and "textually later but executed earlier in a loop" cases both read as satisfied. This looks like a deliberate false-negative trade-off (better than false positives here); worth one sentence in BRT006.md next to the existing nested-block note so it is not mistaken for flow analysis.

6. The fix produces two different layouts for near-identical input

new KafkaPublication() becomes new KafkaPublication() { Partitioner = Partitioner.Murmur2Random } (single line, keeping the now-redundant ()), while new() becomes a multi-line initializer block. Both are pinned by tests, so this is intentional-by-accident rather than a bug. Dropping the empty argument list when adding an initializer (new KafkaPublication { ... }) and picking one layout would make the fix feel less arbitrary.

7. Nullable is not enabled on the new CodeFixes project

Paramore.Brighter.Analyzer.csproj gains <Nullable>enable</Nullable> in this PR (good — and the string? / Location? annotations on the existing visitors follow from it), but Paramore.Brighter.Analyzer.CodeFixes.csproj does not set it, while its code already uses root!. Enabling it there keeps the two projects consistent and makes the ! meaningful.

8. Coverage gap: ambiguous Partitioner, not just a missing using

samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs:102 needs the fully-qualified Paramore.Brighter.MessagingGateway.Kafka.Partitioner.Murmur2Random because that file also has using Confluent.Kafka;, which defines its own Partitioner enum. That is the realistic Kafka scenario, and the fix depends on Simplifier declining to reduce there. The current test (When_Kafka_Using_Is_Missing_Should_Add_Fully_Qualified_Partitioner) covers "using absent"; a test with both usings present would cover the case the sample proves happens in real code.


Nits

  • src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.Package.props:3-4$(MSBuildThisFileDirectory)\..\configuration\... yields a doubled separator (MSBuildThisFileDirectory already ends in one) and hardcodes backslashes. Pre-existing content carried through the rename, but this props file is being packed and evaluated for the first time, so it is the moment to make it $(MSBuildThisFileDirectory)../configuration/....
  • The same two-line // Murmur2Random is recommended: ... comment is duplicated across 10 sample files. Since the rationale now lives in BRT006–008, a single short line (or none) would read lighter and will not drift out of sync with the docs.
  • release_notes.md is not updated. Consumers of the analyzer package get three new warnings on upgrade — and hard build failures under TreatWarningsAsErrors. Given the deliberate choice to ship BRT006 at Warning, an upgrade note pointing at both the per-rule suppression and the newly-working BrighterAnalysisMode=None switch would save people a bisect.
  • BrighterAnalyzerGlobals is a public class holding only consts — static class while you are already reformatting the file. Pre-existing.

@lillo42

lillo42 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

ConstructorAssignsPartitioner over-matching (finding 1): accepted as a trade-off, and it's the cheap direction to be wrong. The constructor check is intentionally syntactic (analyzers must not call Compilation.GetSemanticModel RS1030), so it's a heuristic by design. Its job is to prevent the expensive failure: BRT006 firing on every instantiation of a subclass that configures itself in its constructor, with a Fix All then injecting initializers that silently override the subclass's own choice at every call site. The false negative you describe, a constructor that only configures a different publication inside an object initializer, is contrived by comparison, self-correcting (set the partitioner explicitly or suppress), and doesn't cascade through Fix All. If it shows up in real code we can tighten the scan to skip assignments inside InitializerExpressionSyntax ancestors in a follow-up; I don't want to grow the heuristic further without evidence.

Descriptor wording vs BRT007.md (finding 4): this one is a deliberate maintainer decision, not an oversight. The rule's stated position is that the Murmur2 family is preferred for key-distribution reasons, and the descriptor text and sample comments reflect that. Not changing it in this PR.

Two-layout fix output (finding 6, second half): the empty argument list is now dropped (new KafkaPublication { … }), which was the arbitrary-looking part. Single- vs multi-line layout is left to the formatter; the tests pin the current shapes.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review: Kafka publication partitioner analyzer (BRT006–BRT008)

I read the analyzer, the visitor, both code-fix providers, the packaging changes, the docs and all the tests. This is high-quality analyzer work — a few things worth calling out as genuinely well done before the findings:

  • RegisterCompilationStartAction resolves KafkaPublication/Partitioner once and bails out entirely for compilations that don't reference the Kafka gateway, so non-Kafka solutions pay nothing for the SimpleAssignment callback. That's the right shape.
  • Comparison is by symbol, not by name, throughout — GetPartitionerValueName requires fieldReference.Field.ContainingType to be the Kafka Partitioner enum, and When_Value_Is_A_User_Field_Named_Like_The_Enum_Member_Should_Not_Report is exactly the test that should exist for it.
  • constructorCheckCache memoises the per-type constructor walk; the syntactic ctor check with the RS1030 rationale in a comment is the correct workaround.
  • Reporting on objectCreation.Type / the new keyword rather than the whole creation keeps a 20-line initializer from being squiggled in full.
  • CompilerDiagnostics.Errors in BaseKafkaAnalyzerTest/BaseCodeFixTest means the fixed code is proven to compile, and the trivia handling in AddInitializerExpression is backed by tests for single-line, multi-line, trailing comment and trailing comma. That is more care than most code fixes get.

Not re-litigating the two maintainer decisions in the PR body (BRT006 at Warning/enabled-by-default, and Fix All staying available) — both are argued and documented.


1. The IDE-facing rule descriptions contradict BRT008.md on hot partitions

ConsistentPartitionerRule.description (KafkaPublicationPartitionerAnalyzer.cs:72):

'Murmur2' spreads keys more evenly across partitions than the CRC32-based 'Consistent', avoiding hot partitions.

But docs/BRT008.md:11 correctly says the opposite about the one case that actually produces a hot partition:

Like Consistent, Murmur2 pins messages with empty or NULL keys to a single partition.

So for the value the rule recommends, the headline benefit in the description is not delivered. description is what a user sees in the IDE tooltip and in the build log when triaging a default-on warning, and it's the shortest path to a wrong conclusion here.

Relatedly, the strongest justification — that murmur2 is what the Java producer / librdkafka default uses, so a key lands on the same partition across clients — is the lead argument in BRT007.md:7 and BRT008.md:7 but appears in neither rule description. Suggest swapping the framing in both descriptions to the cross-client-compatibility argument, which is defensible without caveats.

2. "spreads keys more evenly than CRC32" is an unsupported claim

This appears in both rule descriptions, in BRT008.md:9 (a whole paragraph built on it), and copy-pasted into 10 sample files. CRC32 and MurmurHash2 are both effectively uniform over arbitrary keys; there's no distribution advantage to point at. The uneven-load argument that does hold is Consistent/Murmur2 vs *Random for keyless messages — a different axis, and already covered correctly in BRT008.md:11. I'd cut the CRC32-uniformity paragraph rather than leave a default-on warning resting on it.

3. When_Value_Is_A_Cast_Should_Not_Offer_Fix doesn't assert that no fix is offered

PartitionerValueCodeFixProviderTest.cs:271 sets TestCode and ExpectedDiagnostics but no FixedCode. In Microsoft.CodeAnalysis.Testing, CodeFixTest only runs fix verification when the fixed state has sources or an explicit inheritance mode; with FixedCode unset it degrades to analyzer-only verification, so RegisterCodeFixesAsync is never exercised and the CanRewrite gate is never hit.

That matters because CanRewrite exists specifically to stop the provider emitting code that doesn't compile, and this is its only test. Fix:

testContext.FixedCode = testContext.TestCode;   // assert the fix leaves the source unchanged

Worth confirming empirically — temporarily loosen CanRewrite to => true and check the test still passes. If it does, the gate is currently unguarded.

4. New rules were added to AnalyzerReleases.Shipped.md, not Unshipped.md

AnalyzerReleases.Unshipped.md exists and is empty; BRT006–008 were appended to the existing ## Release 1.0 table in Shipped.md. The Roslyn release-tracking convention is new rules → Unshipped.md, promoted into a new Shipped.md release section when a version ships. As written the file asserts these three rules shipped in 1.0, which makes the table unusable for "what changed in this version" once someone needs that.

5. BRT006 false positive: creation in a nested block, assignment in the enclosing block

HasPartitionerAssignmentAfterConstruction walks up to the nearest enclosing IBlockOperation (KafkaPublicationPartitionerAnalyzer.cs:301-310), so:

KafkaPublication publication;
if (useLegacyTopic)
{
    publication = new KafkaPublication { Topic = legacy };   // BRT006
}
else
{
    publication = new KafkaPublication { Topic = current };  // BRT006
}
publication.Partitioner = Partitioner.Murmur2Random;         // not seen

reports BRT006 twice even though the partitioner is unconditionally set. The rule already looks down into nested blocks (When_Consistent_Is_Set_In_Nested_Block_... covers that), so the asymmetry reads as accidental rather than a deliberate boundary — and BRT006.md:18 describes the limitation as "assignments made elsewhere (helper methods, other blocks)", which sounds like only cross-method cases are missed.

Walking up to the outermost IBlockOperation (or the enclosing method body) would make it symmetric at no extra cost. If you'd rather keep the narrow scope, a negative test pinning this shape plus a sentence in BRT006.md would at least make it a known boundary.

6. Fix All across BRT007 + BRT008 only fixes one rule per invocation

equivalenceKey embeds the target value (PartitionerValueCodeFixProvider.cs:83), so a solution containing both Consistent and ConsistentRandom needs two Fix All passes. That's arguably correct (they are different fixes), but FixAll_Should_Replace_All_Discouraged_Values uses two Consistent sites only, so the mixed case is neither tested nor documented. A mixed-rule Fix All test would pin whichever behaviour is intended.


Packaging

This PR quietly fixes a latent bug and the description doesn't say so. Before this change BrighterAnalysisMode could not have worked for anyone:

  • build/Paramore.Brighter.Analyzer.props didn't match the PackageId (Paramore.Brighter.Analyzer.Package), so NuGet would never auto-import it;
  • it wasn't packed at all — no Pack="true" entry existed for it or for configuration/*.editorconfig;
  • the two configs lacked is_global = true, which GlobalAnalyzerConfigFiles requires.

All three are fixed here. That means BRT001–005 consumers who set BrighterAnalysisMode=None to silence the existing rules were silently not being honoured and now will be — a behaviour change worth a line in the PR body and the release notes, independent of the Kafka rules.

Three follow-ups on the packaging:

  • 7. build/ doesn't flow transitively. If the analyzer package is ever picked up indirectly, BrighterAnalysisMode won't apply. Shipping the same props to buildTransitive/ as well is a one-line change and the usual belt-and-braces.
  • 8. Nothing verifies the produced .nupkg. _AddAnalyzersToOutput now globs two DLLs out of $(OutputPath) per TFM, and Paramore.Brighter.Analyzer.CodeFixes.dll sits in analyzers/dotnet/cs while depending on Microsoft.CodeAnalysis.CSharp.Workspaces — present in IDE hosts but not in a plain csc invocation. A dotnet pack + content assertion, or at minimum a manual dotnet build of a consuming project confirming no CS8032/AD0001, would derisk the part of this PR that tests can't currently reach.
  • 9. build/Paramore.Brighter.Analyzer.Package.props has no trailing newline (inherited from the file it replaces). Trivial.

Minor

  • 10. The same three-line // Murmur2Random is recommended: ... avoiding hot partitions comment is duplicated across 10 sample files. Once the wording in §1/§2 is settled, consider putting the rationale in one sample and having the others just set the value (or link BRT007.md) — otherwise every future correction is a 10-file edit.
  • 11. samples/Analyzer/AnalyzerSamples has a sample per existing rule but nothing for BRT006–008, and the CodeFixes project isn't referenced there, so neither the new rules nor the fixes get end-to-end exercise in the sample project. Adding a KafkaPublicationSample.cs needs the Kafka gateway reference, so understandable if it's deliberate — but worth a note if so.
  • 12. The BOM removal and string?/Location? annotations in RequestTypeAssignmentVisitor.cs and SubscriptionConstructorVisitor.cs, plus the reformat of BaseAnalyzerTest.cs, are consequences of adding <Nullable>enable</Nullable> to the analyzer project rather than drive-by edits. Per the repo's change-scope guidance, saying that explicitly in the PR body saves the next reviewer the same detour.

Nothing here blocks except §3 (a test that doesn't test what it claims) and §1 (guidance text that contradicts its own doc); §5 is the one most worth recording a decision on.

Note: CI build was still pending while reviewing, so this is a read-only review — I was not able to compile or run the test suite in this environment.

@lillo42
lillo42 marked this pull request as draft July 27, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant