Skip to content

refactor: Move unified type resolution to Analyzers.Common #46

@DrBarnabus

Description

@DrBarnabus

Context

The source generator's Parser (SourceGenerator.Parser.cs) and the IDE-time PropertyAnalyzer (PropertyAnalyzer.cs) both independently detect whether a property's type implements ISpanParsable<T> and ISpanFormattable. This interface detection logic is currently duplicated four times across the codebase:

  1. Parser, single properties (lines 257-259) — checks typeSymbol.AllInterfaces for ISpanParsable and ISpanFormattable when processing a regular PropertyKeyPart.
  2. Parser, repeating properties (lines 355-357) — performs the identical check on innerTypeSymbol.AllInterfaces when processing a RepeatingPropertyKeyPart (the inner type extracted from List<T>, IReadOnlyList<T>, etc.).
  3. PropertyAnalyzer, CreatePropertyTypeInfo (lines 318-320) — checks property.Type.AllInterfaces for the same two interfaces.
  4. PropertyAnalyzer, CreateInnerTypeInfo (lines 291-293) — checks the inner type's AllInterfaces for generic collection properties.

Inconsistent display formats

The Parser and Analyzer use different SymbolDisplayFormat settings when comparing interface names:

  • Parser uses SymbolDisplayFormat.FullyQualifiedFormat, producing strings like "global::System.ISpanParsable<global::MyType>", then checks with StartsWith("global::System.ISpanParsable").
  • PropertyAnalyzer uses the default display format (no explicit format argument), producing strings like "System.ISpanParsable<MyType>", then checks with StartsWith("System.ISpanParsable", StringComparison.Ordinal).

Both approaches work correctly in practice (via StartsWith for ISpanParsable since it is generic, and Equals for ISpanFormattable), but the inconsistency is a maintenance hazard and makes it harder to reason about whether the analyzer diagnostics and the generator behaviour will always agree.

Why Analyzers.Common is the right home

Analyzers.Common (src/CompositeKey.Analyzers.Common/) already contains the shared PropertyValidation class with PropertyTypeInfo, ValidatePropertyFormat, ValidatePropertyTypeCompatibility, and GetFormattedLength. Both the SourceGeneration project and the Analyzers project reference Analyzers.Common:

  • CompositeKey.SourceGeneration.csproj has a <ProjectReference> to CompositeKey.Analyzers.Common.
  • CompositeKey.Analyzers.csproj also references it.

Moving the interface detection into Analyzers.Common creates a single source of truth, ensuring that the analyzer's real-time diagnostics always match the generator's code emission decisions. If the detection logic ever needs to change (e.g., to support additional interfaces), it only needs to change in one place.

Relationship to #43

Issue #43 unifies the Parser's model types and merges its two separate type resolution code paths (for single and repeating properties) into one. After #43 lands, the Parser will have one copy of the interface detection code instead of two. However, it will still be duplicated between the Parser and the PropertyAnalyzer. This issue addresses that remaining duplication by extracting the shared logic into Analyzers.Common.

Blocked by

Tasks

Extract shared interface detection method into Analyzers.Common

Add a method to PropertyValidation (or a new utility class in Analyzers.Common) that accepts an ITypeSymbol and returns the IsSpanParsable and IsSpanFormattable booleans. The method signature should also accept any additional context needed for type comparisons (e.g., known type symbols for Guid, String) so that the full PropertyTypeInfo can be constructed in one place.

A possible signature:

public static PropertyTypeInfo CreatePropertyTypeInfo(
    ITypeSymbol typeSymbol,
    INamedTypeSymbol? guidType,
    INamedTypeSymbol? stringType)

This accepts the type symbol directly and the resolved well-known types needed for IsGuid/IsString checks, keeping Analyzers.Common free of any dependency on the Parser's KnownTypeSymbols class.

Resolve the display format inconsistency

When creating the shared method, the implementer needs to choose a consistent approach for interface detection. Options include:

  1. Standardise on one SymbolDisplayFormat — pick either FullyQualifiedFormat or the default, and use it consistently.
  2. Avoid string comparison entirely — use ITypeSymbol / INamedTypeSymbol equality or check the interface's ContainingNamespace and MetadataName directly, which is more robust than string matching.

Option 2 is preferred as it eliminates the format sensitivity altogether.

Update consumers

  1. Update the Parser (SourceGenerator.Parser.cs) to call the shared method instead of inline interface detection. After refactor: Unify model layer and type resolution #43, this will be a single call site.
  2. Update the PropertyAnalyzer's CreatePropertyTypeInfo and CreateInnerTypeInfo methods to call the same shared method, replacing their inline detection code.

Verify

  1. All analyzer unit tests pass (CompositeKey.Analyzers.UnitTests, CompositeKey.Analyzers.Common.UnitTests).
  2. All source generation unit tests pass (CompositeKey.SourceGeneration.UnitTests) — including snapshot tests.
  3. All functional tests pass (CompositeKey.SourceGeneration.FunctionalTests).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions