Skip to content

feat(analyzers): ship QuerySpec.Analyzers package with QSPEC0001/2/3 diagnostics and code fixes#153

Merged
AbongileBoja merged 2 commits intodevelopfrom
feat/analyzers-package-151
Apr 27, 2026
Merged

feat(analyzers): ship QuerySpec.Analyzers package with QSPEC0001/2/3 diagnostics and code fixes#153
AbongileBoja merged 2 commits intodevelopfrom
feat/analyzers-package-151

Conversation

@AbongileBoja
Copy link
Copy Markdown
Owner

Summary

Ships QuerySpec.Analyzers — a standalone Roslyn analyzer + code-fix NuGet package that powers the 3.x → 4.0 migration window for the three deprecated APIs introduced in #84:

Diagnostic Trigger Replacement
QSPEC0001 GeoLocation.Latitude / .Longitude member access GeoCoordinate (call legacy.ToGeoCoordinate().Latitude)
QSPEC0002 new AdvancedFilterExpression { ... } FilterSpec (immutable record with init accessors)
QSPEC0003 ICacheProvider.GetAsync<T> / SetAsync<T> invocation ICacheStore.TryGetAsync<T> / SetValueAsync<T>

All three carry one-click code fixes and Fix-All-In-Document/Project/Solution support via WellKnownFixAllProviders.BatchFixer.

Closes #151.

Files added

src/QuerySpec.Analyzers/                  -> ships as analyzers/dotnet/cs/QuerySpec.Analyzers.dll
  QuerySpec.Analyzers.csproj              netstandard2.0, IsPackable=true, IncludeBuildOutput=false
  DiagnosticIds.cs                        QSPEC0001/2/3 string consts
  DiagnosticDescriptors.cs                static readonly DiagnosticDescriptor singletons
  GeoLocationMemberAccessAnalyzer.cs      QSPEC0001 — SimpleMemberAccessExpression registration
  AdvancedFilterExpressionAnalyzer.cs     QSPEC0002 — ObjectCreationExpression registration
  CacheProviderInvocationAnalyzer.cs      QSPEC0003 — InvocationExpression registration
  AnalyzerReleases.{Shipped,Unshipped}.md release-tracking AdditionalFiles
  PublicAPI.{Shipped,Unshipped}.txt       PublicAPI baselines

src/QuerySpec.Analyzers.CodeFixes/        -> ships as analyzers/dotnet/cs/QuerySpec.Analyzers.CodeFixes.dll
  QuerySpec.Analyzers.CodeFixes.csproj    netstandard2.0, IsPackable=false (packed by sibling)
  GeoLocationMemberAccessCodeFixProvider.cs   rewrites legacy.Latitude -> legacy.ToGeoCoordinate().Latitude
  AdvancedFilterExpressionCodeFixProvider.cs  rewrites new AdvancedFilterExpression { ... } -> new FilterSpec { ... }
  CacheProviderInvocationCodeFixProvider.cs   rewrites SetAsync -> SetValueAsync; await GetAsync<T>(k) -> (await TryGetAsync<T>(k)).GetValueOrDefault()

tests/QuerySpec.Analyzers.Tests/          27 tests x 3 TFMs (net8/9/10) = 81 green
  QuerySpec.Analyzers.Tests.csproj
  GeoLocationMemberAccessAnalyzerTests.cs
  AdvancedFilterExpressionAnalyzerTests.cs
  CacheProviderInvocationAnalyzerTests.cs
  Verifiers/CSharpAnalyzerVerifier.cs     wraps CSharpAnalyzerTest with DefaultVerifier
  Verifiers/CSharpCodeFixVerifier.cs      wraps CSharpCodeFixTest with DefaultVerifier
  Verifiers/QuerySpecReferenceAssemblies.cs   pinned to .NET 8 ReferenceAssemblies for stable BCL surface
  Verifiers/TestStubs.cs                  inline-stub the QuerySpec.Core types so tests don't pull
                                          [Obsolete(DiagnosticId)]-emitted compiler warnings into the
                                          analyzer's diagnostic stream — analyzer logic is exercised
                                          in isolation

QuerySpec.sln updated; CHANGELOG [Unreleased] -> Added entry added.

Architecture decisions

  1. Standalone NuGet package, not bundled into QuerySpec.Core. Microsoft's canonical pattern (Microsoft.AspNetCore.App.Analyzers, System.Text.Json.SourceGeneration) lets consumers update analyzers independently of runtime. Adding the analyzer DLL inside QuerySpec.Core's nupkg would lock the analyzer cadence to the runtime cadence and leak the analyzer's Microsoft.CodeAnalysis.* dependency edges into the runtime package's metadata.

  2. Two-assembly split (QuerySpec.Analyzers + QuerySpec.Analyzers.CodeFixes). RS1038 forbids analyzer assemblies from referencing Microsoft.CodeAnalysis.Workspaces (which only loads in IDE/CLI compile-only scenarios fail otherwise). Code fixes legitimately need Workspaces. Both DLLs ship in the same nupkg under analyzers/dotnet/cs/.

  3. netstandard2.0 TFM. This is the documented exception to the repo-wide net8/9/10 policy (Directory.Build.props comment + <TargetFramework> override + this PR's CHANGELOG entry): Roslyn loads analyzers into a netstandard2.0 ALC regardless of the consumer project's target.

  4. Self-suppression on [Obsolete(DiagnosticId="QSPEC####")]. The 3.1 source already carries those attributes, so the compiler emits the diagnostic and our analyzer would otherwise duplicate it. Each analyzer walks the candidate symbol's attributes (and, for ICacheProvider, the implemented interface members) and skips emission when a matching DiagnosticId is found. Net result: consumers see exactly one warning per call site, sourced from whichever path is available.

  5. Code-fix scope: the QSPEC0003 fix deliberately does NOT auto-rewrite the variable's declared type from ICacheProvider to ICacheStore. That's a cross-file decision the developer should make in context (DI registration, downstream usages). The fix produces well-formed code only when the receiver implements ICacheStore; otherwise the developer gets a compile error guiding them to the next migration step. Test cases cover the well-typed scenario via a dual-interface TestCacheProvider stub matching the shipping MemoryCacheProvider/DistributedCacheProvider/MultiLevelCache shape.

Test plan

  • dotnet build -c Release -p:QUERYSPEC_SKIP_SIGN=true — clean (0 errors, 624 pre-existing Meziantou warnings unchanged)
  • dotnet test tests/QuerySpec.Analyzers.Tests/QuerySpec.Analyzers.Tests.csproj — 27 tests x 3 TFMs (net8/9/10) = 81 green
  • dotnet pack src/QuerySpec.Analyzers/QuerySpec.Analyzers.csproj -c Release -p:QUERYSPEC_SKIP_SIGN=true — produces valid .nupkg
  • unzip -l QuerySpec.Analyzers.0.0.0-local.nupkg — confirms analyzers/dotnet/cs/QuerySpec.Analyzers.dll and analyzers/dotnet/cs/QuerySpec.Analyzers.CodeFixes.dll, README, icon present
  • dotnet test tests/QuerySpec.Core.Tests/... — 636/636 net8 (no regression, analyzer is consumer-side)
  • dotnet test tests/QuerySpec.EFCore.Tests/... — 91/91 net8 (no regression)
  • CI green across all gates (build/test/codeql/format/vuln/repro/dependency-review)

Per-test coverage matrix (each analyzer + code-fix pair):

  • Positive trigger (read/create/invoke)
  • Negative on the replacement type (GeoCoordinate, FilterSpec, ICacheStore)
  • Negative on an unrelated same-name symbol
  • #pragma warning disable QSPEC#### suppression
  • Self-suppression when source already carries [Obsolete(DiagnosticId)]
  • HelpLinkUri constant verification
  • Single-occurrence code fix
  • Multi-occurrence FixAll

…ostics and code fixes

Adds two new netstandard2.0 projects (QuerySpec.Analyzers and QuerySpec.Analyzers.CodeFixes,
packed together in one nupkg under analyzers/dotnet/cs/) plus a tri-TFM xUnit test project
covering the three migration diagnostics:

- QSPEC0001: GeoLocation.Latitude/.Longitude member access -> GeoCoordinate
- QSPEC0002: AdvancedFilterExpression object creation -> FilterSpec
- QSPEC0003: ICacheProvider.GetAsync/SetAsync invocation -> ICacheStore.TryGetAsync/SetValueAsync

Each analyzer registers narrowly-scoped syntax-kind callbacks (member access, object creation,
invocation respectively), matches via the semantic model rather than syntax text, and suppresses
itself when the target member already carries a matching [Obsolete(DiagnosticId)] attribute so
consumers see exactly one warning per call site. Code fix providers use BatchFixer for FixAll
support and emit well-formed compilable replacements; the cache provider rewrite leaves any
needed variable-typing change to the developer (documented in the fix's title and message).

netstandard2.0 is the documented exception to the repo TFM policy because Roslyn loads analyzers
into a netstandard2.0 ALC regardless of the consumer's target. RS1038 enforces analyzer
assemblies cannot reference Microsoft.CodeAnalysis.Workspaces, so analyzers and code fixes ship
in separate sibling projects (Microsoft canonical pattern).

27 tests x 3 TFMs = 81 green; zero analyzer warnings; nupkg layout verified.

Refs #151
Comment thread src/QuerySpec.Analyzers/AdvancedFilterExpressionAnalyzer.cs
Comment thread src/QuerySpec.Analyzers/CacheProviderInvocationAnalyzer.cs
Comment thread src/QuerySpec.Analyzers/CacheProviderInvocationAnalyzer.cs
Comment thread src/QuerySpec.Analyzers/GeoLocationMemberAccessAnalyzer.cs
Copy link
Copy Markdown
Owner Author

@AbongileBoja AbongileBoja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quality-reviewer gate: PASS. All checklist items verified. Build/test/format/reproducibility/vuln-scan/dep-review/commitlint: pass. CodeQL: pass. See full review summary in PR dispatch chat.

Replace placeholder QuerySpec.Core icon with the dedicated Analyzers logo.
Resized to 512x512 with white padding to honor NuGet's <=1MB icon limit
(NU5047) and the square-icon convention used on nuget.org.

Refs #151
@AbongileBoja AbongileBoja merged commit c9032a8 into develop Apr 27, 2026
11 of 14 checks passed
@AbongileBoja AbongileBoja deleted the feat/analyzers-package-151 branch April 27, 2026 12:05
AbongileBoja added a commit that referenced this pull request Apr 29, 2026
Microsoft.Sbom.Targets 4.1.5 handles IncludeBuildOutput=false analyzer-shape
packs correctly. The GenerateSBOM=false override from PR #153 was set when
Sbom.Targets 3.x could not emit the manifest into a no-build-output pack;
that limitation no longer exists. Removing the override lets Directory.Build.props
drive SBOM generation for all packable projects uniformly.

Fixes #248
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build QuerySpec.Analyzers Roslyn analyzer + code fixes for QSPEC0001/0002/0003 migrations

2 participants