diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2196ffd..cb4a5f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,22 +13,15 @@ All notable changes to ANcpLua.Analyzers will be documented in this file.
### Added
-- AL0107: Orphaned `[TracedTag]` on parameters of methods without `[Traced]` attribute
-- AL0108: Redundant `[NoTrace]` on methods that are not interceptable
-- AL0109: `[Traced]` on non-interceptable methods (non-partial, private, static non-partial)
-- AL0110: `[TracedTag]` on `out`/`ref` parameters (cannot be captured for span attributes)
-- Code fixes for AL0107, AL0108, AL0109, AL0110
- GitHub Actions workflow to remind about .NET 11 GA adoption on 2026-12-01
- Dependabot ignore rule for .NET 11 SDK previews/RCs
### Fixed
-- README.md diagnostic count (102 → 106) and code fix count (38 → 42)
-- README.md OpenTelemetry category count (19 → 23)
-- README.md rule table missing AL0107-AL0110 rows
- NuGet package description out of sync with actual diagnostic/code fix counts
- Stale `nupkg/` directory (pre-UseArtifactsOutput artifact)
### Removed
+- Telemetry-adjacent diagnostics and code fixes now homed in `ANcpLua.OpenTelemetry.SemanticConventions.Analyzers`: AL0013, AL0061, AL0063, AL0064, AL0065, AL0066, AL0067, AL0068, AL0069, AL0070, AL0071, AL0072, AL0073, AL0074, AL0075, AL0076, AL0077, AL0078, AL0079, AL0083, AL0085, AL0086, AL0088, AL0089, AL0090, AL0091, AL0092, AL0093, AL0096, AL0107, AL0108, AL0109, AL0110, AL0113, AL0124, AL0131, AL0135
- `verify-published-versions.ps1` — superseded by AL0017/AL0018/AL0019 compile-time enforcement
diff --git a/README.md b/README.md
index 7750192..086dba3 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
# ANcpLua.Analyzers
-Roslyn analyzers for modern C# patterns, OpenTelemetry instrumentation, AOT safety, and ANcpLua ecosystem conventions.
+Roslyn analyzers for modern C# patterns, AOT safety, ASP.NET Core, reliability, GenAI tool governance, and ANcpLua ecosystem conventions.
## Installation
@@ -16,27 +16,25 @@ dotnet add package ANcpLua.Analyzers
## What you get
-- **127 diagnostics** spanning design, reliability, performance, OpenTelemetry, ASP.NET Core, AOT, and version management.
-- **46 automatic code fixes** for high-confidence transformations.
+- **88 diagnostics** spanning design, reliability, usage, Roslyn utilities, ASP.NET Core, AOT, threading, style, configuration, GenAI, and version management.
+- **36 automatic code fixes** for high-confidence transformations.
- **CI-friendly enforcement** through `.editorconfig` severity configuration.
## Rule coverage by category
| Category | Rules |
|:---------|------:|
-| OpenTelemetry | 24 |
| Roslyn Utilities | 24 |
| AOT Testing | 10 |
+| Reliability | 10 |
| ASP.NET Core | 9 |
-| GenAI | 9 |
-| Reliability | 9 |
| Usage | 9 |
-| Design | 7 |
| VersionManagement | 7 |
+| Design | 6 |
| Threading | 6 |
-| Configuration | 5 |
-| Metrics | 5 |
+| GenAI | 3 |
| Style | 3 |
+| Configuration | 1 |
## Full rule catalog
@@ -52,7 +50,6 @@ dotnet add package ANcpLua.Analyzers
| [AL0008](https://ancplua.mintlify.app/analyzers/rules/AL0008) | Usage | Error | `Al0007ToAl0009IXmlSerializableAnalyzer` |
| [AL0009](https://ancplua.mintlify.app/analyzers/rules/AL0009) | Usage | Error | `Al0007ToAl0009IXmlSerializableAnalyzer` |
| [AL0011](https://ancplua.mintlify.app/analyzers/rules/AL0011) | Threading | Warning | `Al0011LockKeywordAnalyzer` |
-| [AL0013](https://ancplua.mintlify.app/analyzers/rules/AL0013) | OpenTelemetry | Info | `Al0013MissingSchemaUrlAnalyzer` |
| [AL0014](https://ancplua.mintlify.app/analyzers/rules/AL0014) | Style | Warning | `Al0014PreferPatternMatchingAnalyzer` |
| [AL0015](https://ancplua.mintlify.app/analyzers/rules/AL0015) | Style | Info | `Al0015NormalizeNullGuardStyleAnalyzer` |
| [AL0016](https://ancplua.mintlify.app/analyzers/rules/AL0016) | Style | Info | `Al0016CombineDeclarationWithNullCheckAnalyzer` |
@@ -77,7 +74,6 @@ dotnet add package ANcpLua.Analyzers
| [AL0035](https://ancplua.mintlify.app/analyzers/rules/AL0035) | Roslyn Utilities | Info | `Al0035UseToDisplayStringExtensionsAnalyzer` |
| [AL0036](https://ancplua.mintlify.app/analyzers/rules/AL0036) | Roslyn Utilities | Warning | `Al0036UseGuardNotNullAnalyzer` |
| [AL0037](https://ancplua.mintlify.app/analyzers/rules/AL0037) | Roslyn Utilities | Warning | `Al0037UseTryParseExtensionsAnalyzer` |
-| [AL0038](https://ancplua.mintlify.app/analyzers/rules/AL0038) | Roslyn Utilities | Warning | `Al0038UseGetOrNullAnalyzer` |
| [AL0039](https://ancplua.mintlify.app/analyzers/rules/AL0039) | Roslyn Utilities | Warning | `Al0039UseStringComparisonExtensionsAnalyzer` |
| [AL0040](https://ancplua.mintlify.app/analyzers/rules/AL0040) | Roslyn Utilities | Warning | `Al0040UseAttributeExtensionsAnalyzer` |
| [AL0041](https://ancplua.mintlify.app/analyzers/rules/AL0041) | AOT Testing | Error | `Al0041AotTestMustReturnIntAnalyzer` |
@@ -100,53 +96,20 @@ dotnet add package ANcpLua.Analyzers
| [AL0058](https://ancplua.mintlify.app/analyzers/rules/AL0058) | Threading | Warning | `Al0057ToAl0060ThreadingAnalyzer` |
| [AL0059](https://ancplua.mintlify.app/analyzers/rules/AL0059) | Threading | Warning | `Al0057ToAl0060ThreadingAnalyzer` |
| [AL0060](https://ancplua.mintlify.app/analyzers/rules/AL0060) | Threading | Warning | `Al0057ToAl0060ThreadingAnalyzer` |
-| [AL0061](https://ancplua.mintlify.app/analyzers/rules/AL0061) | OpenTelemetry | Warning | `Al0061ActivityMissingSemconvAnalyzer` |
-| [AL0063](https://ancplua.mintlify.app/analyzers/rules/AL0063) | OpenTelemetry | Warning | `Al0063UnregisteredActivitySourceAnalyzer` |
-| [AL0064](https://ancplua.mintlify.app/analyzers/rules/AL0064) | GenAI | Warning | `Al0064GenAiMissingRequiredAttributesAnalyzer` |
-| [AL0065](https://ancplua.mintlify.app/analyzers/rules/AL0065) | GenAI | Warning | `Al0065UseTokenUsageHistogramAnalyzer` |
-| [AL0066](https://ancplua.mintlify.app/analyzers/rules/AL0066) | GenAI | Warning | `Al0066InvalidGenAiOperationNameAnalyzer` |
-| [AL0067](https://ancplua.mintlify.app/analyzers/rules/AL0067) | Metrics | Warning | `Al0067UnregisteredMeterAnalyzer` |
-| [AL0068](https://ancplua.mintlify.app/analyzers/rules/AL0068) | Metrics | Warning | `Al0068InvalidMetricNameAnalyzer` |
-| [AL0069](https://ancplua.mintlify.app/analyzers/rules/AL0069) | Configuration | Warning | `Al0069IncompleteServiceDefaultsAnalyzer` |
-| [AL0070](https://ancplua.mintlify.app/analyzers/rules/AL0070) | Configuration | Warning | `Al0070NonOtlpCollectorEndpointAnalyzer` |
-| [AL0071](https://ancplua.mintlify.app/analyzers/rules/AL0071) | Metrics | Error | `Al0071MeterClassMustBePartialStaticAnalyzer` |
-| [AL0072](https://ancplua.mintlify.app/analyzers/rules/AL0072) | Metrics | Error | `Al0072MetricMethodMustBePartialAnalyzer` |
-| [AL0073](https://ancplua.mintlify.app/analyzers/rules/AL0073) | OpenTelemetry | Error | `Al0073TracedActivitySourceNameAnalyzer` |
-| [AL0074](https://ancplua.mintlify.app/analyzers/rules/AL0074) | GenAI | Warning | `Al0074DeprecatedGenAiAttributeAnalyzer` |
-| [AL0075](https://ancplua.mintlify.app/analyzers/rules/AL0075) | Metrics | Warning | `Al0075HighCardinalityMetricTagAnalyzer` |
-| [AL0076](https://ancplua.mintlify.app/analyzers/rules/AL0076) | OpenTelemetry | Warning | `Al0076MissingOTelConfigurationAnalyzer` |
-| [AL0077](https://ancplua.mintlify.app/analyzers/rules/AL0077) | OpenTelemetry | Warning | `Al0077DuplicateInstrumentationAnalyzer` |
-| [AL0078](https://ancplua.mintlify.app/analyzers/rules/AL0078) | OpenTelemetry | Error | `Al0078InvalidActivitySourceNameAnalyzer` |
-| [AL0079](https://ancplua.mintlify.app/analyzers/rules/AL0079) | OpenTelemetry | Info | `Al0079ManualSpanRecommendedAnalyzer` |
| [AL0080](https://ancplua.mintlify.app/analyzers/rules/AL0080) | ASP.NET Core | Warning | `Al0080MissingResilienceConfigurationAnalyzer` |
| [AL0081](https://ancplua.mintlify.app/analyzers/rules/AL0081) | ASP.NET Core | Warning | `Al0081MissingHealthChecksAnalyzer` |
| [AL0082](https://ancplua.mintlify.app/analyzers/rules/AL0082) | Configuration | Info | `Al0082ConsiderConnectionStringAnalyzer` |
-| [AL0083](https://ancplua.mintlify.app/analyzers/rules/AL0083) | Configuration | Warning | `Al0083InsecureEndpointAnalyzer` |
| [AL0084](https://ancplua.mintlify.app/analyzers/rules/AL0084) | ASP.NET Core | Warning | `Al0084MissingServiceDiscoveryAnalyzer` |
-| [AL0085](https://ancplua.mintlify.app/analyzers/rules/AL0085) | OpenTelemetry | Error | `Al0085InvalidAttributeValueAnalyzer` |
-| [AL0086](https://ancplua.mintlify.app/analyzers/rules/AL0086) | OpenTelemetry | Warning | `Al0086IncorrectAttributeTypeAnalyzer` |
-| [AL0088](https://ancplua.mintlify.app/analyzers/rules/AL0088) | OpenTelemetry | Warning | `Al0088SensitiveDataInAttributeAnalyzer` |
-| [AL0089](https://ancplua.mintlify.app/analyzers/rules/AL0089) | OpenTelemetry | Warning | `Al0089MissingOtlpConfigurationAnalyzer` |
-| [AL0090](https://ancplua.mintlify.app/analyzers/rules/AL0090) | OpenTelemetry | Warning | `Al0090UncompressedExportAnalyzer` |
-| [AL0091](https://ancplua.mintlify.app/analyzers/rules/AL0091) | OpenTelemetry | Warning | `Al0091BatchExportDisabledAnalyzer` |
-| [AL0092](https://ancplua.mintlify.app/analyzers/rules/AL0092) | OpenTelemetry | Info | `Al0092ConsiderSamplingAnalyzer` |
-| [AL0093](https://ancplua.mintlify.app/analyzers/rules/AL0093) | OpenTelemetry | Warning | `Al0093MissingResourceAttributesAnalyzer` |
| [AL0094](https://ancplua.mintlify.app/analyzers/rules/AL0094) | AOT Testing | Warning | `Al0094AvoidDynamicKeywordAnalyzer` |
| [AL0095](https://ancplua.mintlify.app/analyzers/rules/AL0095) | AOT Testing | Warning | `Al0095AvoidExpressionCompileAnalyzer` |
-| [AL0096](https://ancplua.mintlify.app/analyzers/rules/AL0096) | Configuration | Warning | `Al0096EnableEventSourceSupportAnalyzer` |
| [AL0101](https://ancplua.mintlify.app/analyzers/rules/AL0101) | AOT Testing | Warning | `Al0101AvoidActivatorCreateInstanceAnalyzer` |
| [AL0102](https://ancplua.mintlify.app/analyzers/rules/AL0102) | AOT Testing | Warning | `Al0102AvoidTypeGetTypeAnalyzer` |
| [AL0103](https://ancplua.mintlify.app/analyzers/rules/AL0103) | Design | Warning | `Al0103ClosedTypeHierarchySwitchAnalyzer` |
| [AL0104](https://ancplua.mintlify.app/analyzers/rules/AL0104) | Reliability | Warning | `Al0104PreferAwaitUsingAnalyzer` |
| [AL0105](https://ancplua.mintlify.app/analyzers/rules/AL0105) | Threading | Warning | `Al0105AvoidBlockingCallsInAsyncAnalyzer` |
| [AL0106](https://ancplua.mintlify.app/analyzers/rules/AL0106) | ASP.NET Core | Warning | `Al0106AvoidTaskRunInAspNetCoreAnalyzer` |
-| [AL0107](https://ancplua.mintlify.app/analyzers/rules/AL0107) | OpenTelemetry | Warning | `Al0107OrphanedTracedTagAnalyzer` |
-| [AL0108](https://ancplua.mintlify.app/analyzers/rules/AL0108) | OpenTelemetry | Info | `Al0108RedundantNoTraceAnalyzer` |
-| [AL0109](https://ancplua.mintlify.app/analyzers/rules/AL0109) | OpenTelemetry | Warning | `Al0109NonInterceptableTracedAnalyzer` |
-| [AL0110](https://ancplua.mintlify.app/analyzers/rules/AL0110) | OpenTelemetry | Error | `Al0110TracedTagOnOutRefParameterAnalyzer` |
| [AL0111](https://ancplua.mintlify.app/analyzers/rules/AL0111) | Reliability | Warning | `Al0111SqlInterpolationInCommandTextAnalyzer` |
| [AL0112](https://ancplua.mintlify.app/analyzers/rules/AL0112) | Reliability | Warning | `Al0112FireAndForgetTaskAnalyzer` |
-| [AL0113](https://ancplua.mintlify.app/analyzers/rules/AL0113) | OpenTelemetry | Warning | `Al0113MissingExceptionRecordingOnActivityAnalyzer` |
| [AL0114](https://ancplua.mintlify.app/analyzers/rules/AL0114) | Reliability | Warning | `Al0114PreferTryParseAnalyzer` |
| [AL0115](https://ancplua.mintlify.app/analyzers/rules/AL0115) | Reliability | Warning | `Al0115EmptyCatchBlockAnalyzer` |
| [AL0116](https://ancplua.mintlify.app/analyzers/rules/AL0116) | Reliability | Warning | `Al0116ExceptionLeakedInResponseAnalyzer` |
@@ -157,14 +120,14 @@ dotnet add package ANcpLua.Analyzers
| [AL0121](https://ancplua.mintlify.app/analyzers/rules/AL0121) | Roslyn Utilities | Warning | `Al0121NormalizeWhitespaceAnalyzer` |
| [AL0122](https://ancplua.mintlify.app/analyzers/rules/AL0122) | Design | Error | `Al0122DuckDbTableMustBePartialAnalyzer` |
| [AL0123](https://ancplua.mintlify.app/analyzers/rules/AL0123) | Design | Warning | `Al0123DuckDbColumnConflictingOrdinalAnalyzer` |
-| [AL0124](https://ancplua.mintlify.app/analyzers/rules/AL0124) | GenAI | Warning | `Al0124NonInterceptableAgentTracedAnalyzer` |
| [AL0125](https://ancplua.mintlify.app/analyzers/rules/AL0125) | Roslyn Utilities | Info | `Al0125UseStringComparisonAnyExtensionsAnalyzer` |
| [AL0126](https://ancplua.mintlify.app/analyzers/rules/AL0126) | Reliability | Info | `Al0126CancellationTokenPropagationAnalyzer` |
| [AL0127](https://ancplua.mintlify.app/analyzers/rules/AL0127) | VersionManagement | Warning | `Al0127OutdatedMafPackageVersionAnalyzer` |
| [AL0128](https://ancplua.mintlify.app/analyzers/rules/AL0128) | GenAI | Warning | `Al0128DestructiveToolMustRequireApprovalAnalyzer` |
| [AL0129](https://ancplua.mintlify.app/analyzers/rules/AL0129) | GenAI | Info | `Al0129ToolMustDeclareSideEffectAnalyzer` |
| [AL0130](https://ancplua.mintlify.app/analyzers/rules/AL0130) | GenAI | Info | `Al0130ToolMustDeclareCapabilityAnalyzer` |
-| [AL0131](https://ancplua.mintlify.app/analyzers/rules/AL0131) | GenAI | Warning | `Al0131DirectGenAiSdkUsageAnalyzer` |
+| [AL0137](https://ancplua.mintlify.app/analyzers/rules/AL0137) | Roslyn Utilities | Warning | `Al0137UseGuardForThrowIfAnalyzer` |
+| [AL0138](https://ancplua.mintlify.app/analyzers/rules/AL0138) | Reliability | Warning | `Al0138UseExplicitMidpointRoundingAnalyzer` |
**Legend:** `Error` = build error, `Warning` = build warning, `Info` = IDE suggestion, `Disabled` = off by default.
@@ -172,7 +135,7 @@ dotnet add package ANcpLua.Analyzers
Automatic fixes are currently available for:
-AL0002, AL0004, AL0005, AL0008, AL0011, AL0014, AL0015, AL0016, AL0025, AL0026, AL0027, AL0028, AL0029, AL0030, AL0031, AL0032, AL0033, AL0034, AL0035, AL0036, AL0037, AL0038, AL0039, AL0040, AL0045, AL0046, AL0047, AL0048, AL0049, AL0050, AL0051, AL0071, AL0072, AL0073, AL0074, AL0103, AL0107, AL0108, AL0109, AL0110, AL0121, AL0122, AL0124, AL0126
+AL0002, AL0004, AL0005, AL0008, AL0011, AL0014, AL0015, AL0016, AL0025, AL0026, AL0027, AL0028, AL0029, AL0030, AL0031, AL0032, AL0033, AL0034, AL0035, AL0036, AL0037, AL0039, AL0040, AL0045, AL0046, AL0047, AL0048, AL0049, AL0050, AL0051, AL0103, AL0121, AL0122, AL0126, AL0137, AL0138
## Configuration
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0071MeterClassCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0071MeterClassCodeFixProvider.cs
deleted file mode 100644
index eeef0bd..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0071MeterClassCodeFixProvider.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0071: Adds 'partial static' modifiers to [Meter] class.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0071MeterClassCodeFixProvider))]
-[Shared]
-public sealed partial class Al0071MeterClassCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0071MeterClassMustBePartialStaticAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not
- { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
-
- // Find the class declaration identified by the diagnostic
- if (root.FindToken(diagnostic.Location.SourceSpan.Start).Parent?
- .AncestorsAndSelf().OfType().FirstOrDefault() is not { } classDeclaration) {
- return;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0071CodeFixTitle,
- c => MakePartialStaticAsync(context.Document, classDeclaration, root, c),
- nameof(Al0071MeterClassCodeFixProvider)),
- diagnostic);
- }
-
- private static Task MakePartialStaticAsync(
- Document document,
- ClassDeclarationSyntax classDeclaration,
- SyntaxNode root,
- CancellationToken _) {
- var modifiers = classDeclaration.Modifiers;
-
- // Check what modifiers we need to add
- var hasPartial = modifiers.Any(SyntaxKind.PartialKeyword);
- var hasStatic = modifiers.Any(SyntaxKind.StaticKeyword);
-
- var newModifiers = modifiers;
-
- // Add static modifier if missing (before partial if partial exists, or at end)
- if (!hasStatic) {
- var staticToken = SyntaxFactory.Token(SyntaxKind.StaticKeyword).WithTrailingTrivia(SyntaxFactory.Space);
-
- // Find position to insert: after access modifiers, before 'partial' or 'class'
- var insertIndex = GetStaticInsertIndex(modifiers);
- newModifiers = newModifiers.Insert(insertIndex, staticToken);
- }
-
- // Add partial modifier if missing (should be right before 'class')
- if (!hasPartial) {
- var partialToken = SyntaxFactory.Token(SyntaxKind.PartialKeyword).WithTrailingTrivia(SyntaxFactory.Space);
- newModifiers = newModifiers.Add(partialToken);
- }
-
- var newClassDeclaration = classDeclaration.WithModifiers(newModifiers);
- var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
-
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-
- private static int GetStaticInsertIndex(SyntaxTokenList modifiers) {
- // Insert static after access modifiers (public, private, protected, internal)
- for (var i = 0; i < modifiers.Count; i++) {
- var kind = modifiers[i].Kind();
- if (kind is not (SyntaxKind.PublicKeyword or SyntaxKind.PrivateKeyword or
- SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword)) {
- return i;
- }
- }
-
- return modifiers.Count;
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0072MetricMethodCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0072MetricMethodCodeFixProvider.cs
deleted file mode 100644
index 317b436..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0072MetricMethodCodeFixProvider.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0072: Adds 'partial' modifier to metric methods.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0072MetricMethodCodeFixProvider))]
-[Shared]
-public sealed partial class Al0072MetricMethodCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0072MetricMethodMustBePartialAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not
- { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
-
- // Find the method declaration identified by the diagnostic
- if (root.FindToken(diagnostic.Location.SourceSpan.Start).Parent?
- .AncestorsAndSelf().OfType().FirstOrDefault() is not { } methodDeclaration) {
- return;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0072CodeFixTitle,
- c => MakePartialAsync(context.Document, methodDeclaration, root, c),
- nameof(Al0072MetricMethodCodeFixProvider)),
- diagnostic);
- }
-
- private static Task MakePartialAsync(
- Document document,
- MethodDeclarationSyntax methodDeclaration,
- SyntaxNode root,
- CancellationToken _) {
- var modifiers = methodDeclaration.Modifiers;
-
- // Check if partial is already present
- if (modifiers.Any(SyntaxKind.PartialKeyword)) {
- return Task.FromResult(document);
- }
-
- // Add partial modifier before the return type
- var partialToken = SyntaxFactory.Token(SyntaxKind.PartialKeyword).WithTrailingTrivia(SyntaxFactory.Space);
- var newModifiers = modifiers.Add(partialToken);
-
- // For partial methods, we also need to:
- // 1. Remove the method body and replace with semicolon
- // 2. Keep the method signature
-
- var newMethodDeclaration = methodDeclaration
- .WithModifiers(newModifiers)
- .WithBody(null)
- .WithExpressionBody(null)
- .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
-
- var newRoot = root.ReplaceNode(methodDeclaration, newMethodDeclaration);
-
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0073TracedCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0073TracedCodeFixProvider.cs
deleted file mode 100644
index 5856d4b..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0073TracedCodeFixProvider.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0073: Adds a default ActivitySourceName to [Traced] attribute.
-///
-///
-///
-/// This code fix provides a default ActivitySourceName based on the containing type's
-/// fully qualified name. For example, a class named MyApp.Services.OrderService
-/// would get the ActivitySourceName "MyApp.Services.OrderService".
-///
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0073TracedCodeFixProvider))]
-[Shared]
-public sealed partial class Al0073TracedCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0073TracedActivitySourceNameAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not
- { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
-
- // Find the attribute syntax
- if (root.FindNode(diagnostic.Location.SourceSpan)
- .AncestorsAndSelf().OfType().FirstOrDefault() is not { } attributeSyntax) {
- return;
- }
-
- // Get semantic model to determine the containing type's name
- if (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is not
- { } semanticModel) {
- return;
- }
-
- // Determine the suggested source name based on context
- var suggestedName = GetSuggestedActivitySourceName(attributeSyntax, semanticModel, context.CancellationToken);
-
- context.RegisterCodeFix(
- CodeAction.Create(
- string.Format(CodeFixResources.AL0073CodeFixTitle, suggestedName),
- c => AddActivitySourceNameAsync(context.Document, attributeSyntax, suggestedName, root, c),
- nameof(Al0073TracedCodeFixProvider)),
- diagnostic);
- }
-
- private static string GetSuggestedActivitySourceName(
- SyntaxNode attribute,
- SemanticModel semanticModel,
- CancellationToken cancellationToken) {
- // Find the containing type
- if (attribute.Ancestors().OfType().FirstOrDefault() is not { } containingType) {
- return "MyApp";
- }
-
- if (semanticModel.GetDeclaredSymbol(containingType, cancellationToken) is not { } typeSymbol) {
- return containingType.Identifier.Text;
- }
-
- // Use the fully qualified name without global:: prefix
- var fullName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
- return fullName.ReplaceOrdinal("global::", "") ?? fullName;
- }
-
- private static Task AddActivitySourceNameAsync(
- Document document,
- AttributeSyntax attribute,
- string sourceName,
- SyntaxNode root,
- CancellationToken _) {
- AttributeSyntax newAttribute;
-
- // Check if attribute has argument list
- if (attribute.ArgumentList is null || attribute.ArgumentList.Arguments.Count is 0) {
- // Create new argument list with the source name
- var argument = SyntaxFactory.AttributeArgument(
- SyntaxFactory.LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- SyntaxFactory.Literal(sourceName)));
-
- var argumentList = SyntaxFactory.AttributeArgumentList(
- SyntaxFactory.SingletonSeparatedList(argument));
-
- newAttribute = attribute.WithArgumentList(argumentList);
- } else {
- // Check if first argument is empty string - replace it
- var firstArg = attribute.ArgumentList.Arguments[0];
- if (firstArg.Expression is LiteralExpressionSyntax { Token.ValueText: "" or " " or " " }) {
- var newArg = SyntaxFactory.AttributeArgument(
- SyntaxFactory.LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- SyntaxFactory.Literal(sourceName)));
-
- var newArguments = attribute.ArgumentList.Arguments.Replace(firstArg, newArg);
- var newArgumentList = attribute.ArgumentList.WithArguments(newArguments);
- newAttribute = attribute.WithArgumentList(newArgumentList);
- } else {
- // Has non-empty arguments but missing ActivitySourceName - add as named argument
- var namedArg = SyntaxFactory.AttributeArgument(
- SyntaxFactory.NameEquals("ActivitySourceName"),
- null,
- SyntaxFactory.LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- SyntaxFactory.Literal(sourceName)));
-
- // Insert at the beginning
- var newArguments = attribute.ArgumentList.Arguments.Insert(0, namedArg);
- var newArgumentList = attribute.ArgumentList.WithArguments(newArguments);
- newAttribute = attribute.WithArgumentList(newArgumentList);
- }
- }
-
- var newRoot = root.ReplaceNode(attribute, newAttribute);
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0074DeprecatedGenAiCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0074DeprecatedGenAiCodeFixProvider.cs
deleted file mode 100644
index a09fbe0..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0074DeprecatedGenAiCodeFixProvider.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0074: Replaces deprecated GenAI attribute names with current ones.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0074DeprecatedGenAiCodeFixProvider))]
-[Shared]
-public sealed partial class Al0074DeprecatedGenAiCodeFixProvider
- : AlCodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0074DeprecatedGenAiAttributeAnalyzer.DiagnosticId];
-
- /// Creates the code action for this fix.
- protected override CodeAction? CreateCodeAction(
- Document document,
- LiteralExpressionSyntax literal,
- SyntaxNode root,
- Diagnostic diagnostic) {
- if (!diagnostic.Properties.TryGetValue("Replacement", out var replacement) || replacement is null) {
- return null;
- }
-
- return CodeAction.Create(
- string.Format(CodeFixResources.AL0074CodeFixTitle, replacement),
- _ => ReplaceAttributeNameAsync(document, literal, replacement, root),
- nameof(Al0074DeprecatedGenAiCodeFixProvider));
- }
-
- private static Task ReplaceAttributeNameAsync(
- Document document,
- SyntaxNode literal,
- string replacement,
- SyntaxNode root) {
- var newLiteral = SyntaxFactory.LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- SyntaxFactory.Literal(replacement))
- .WithTriviaFrom(literal);
-
- var newRoot = root.ReplaceNode(literal, newLiteral);
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0107OrphanedTracedTagCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0107OrphanedTracedTagCodeFixProvider.cs
deleted file mode 100644
index 5531aaa..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0107OrphanedTracedTagCodeFixProvider.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0107: Removes orphaned [TracedTag] attribute from parameters.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0107OrphanedTracedTagCodeFixProvider))]
-[Shared]
-public sealed partial class Al0107OrphanedTracedTagCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0107OrphanedTracedTagAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
- var node = root.FindNode(diagnostic.Location.SourceSpan);
-
- if (node is not ParameterSyntax parameter) {
- return;
- }
-
- if (FindAttributeByName(parameter, "TracedTag") is not { } attributeSyntax) {
- return;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0107CodeFixTitle,
- _ => RemoveAttributeAsync(context.Document, root, attributeSyntax),
- nameof(Al0107OrphanedTracedTagCodeFixProvider)),
- diagnostic);
- }
-
- private static AttributeSyntax? FindAttributeByName(SyntaxNode node, string attributeShortName) {
- foreach (var attributeList in node.ChildNodes().OfType()) {
- foreach (var attribute in attributeList.Attributes) {
- var name = attribute.Name.ToString();
- if (name == attributeShortName || name.EndsWithOrdinal("." + attributeShortName) ||
- name == attributeShortName + "Attribute" ||
- name.EndsWithOrdinal("." + attributeShortName + "Attribute")) {
- return attribute;
- }
- }
- }
-
- return null;
- }
-
- private static Task RemoveAttributeAsync(Document document, SyntaxNode root, AttributeSyntax attribute) {
- var newRoot = RemoveAttribute(root, attribute);
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-
- private static SyntaxNode RemoveAttribute(SyntaxNode root, AttributeSyntax attribute) {
- if (attribute.Parent is not AttributeListSyntax attributeList) {
- return root;
- }
-
- // If this is the only attribute in the list, remove the entire list
- if (attributeList.Attributes.Count == 1) {
- return root.RemoveNode(attributeList, SyntaxRemoveOptions.KeepNoTrivia)!;
- }
-
- // Otherwise, remove just this attribute from the list
- var newList = attributeList.WithAttributes(attributeList.Attributes.Remove(attribute));
- return root.ReplaceNode(attributeList, newList);
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0108RedundantNoTraceCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0108RedundantNoTraceCodeFixProvider.cs
deleted file mode 100644
index f314c05..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0108RedundantNoTraceCodeFixProvider.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0108: Removes redundant [NoTrace] attribute from methods.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0108RedundantNoTraceCodeFixProvider))]
-[Shared]
-public sealed partial class Al0108RedundantNoTraceCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0108RedundantNoTraceAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
- var node = root.FindNode(diagnostic.Location.SourceSpan);
-
- if (node.AncestorsAndSelf().OfType().FirstOrDefault() is not { } method) {
- return;
- }
-
- if (FindAttributeByName(method, "NoTrace") is not { } attributeSyntax) {
- return;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0108CodeFixTitle,
- _ => RemoveAttributeAsync(context.Document, root, attributeSyntax),
- nameof(Al0108RedundantNoTraceCodeFixProvider)),
- diagnostic);
- }
-
- private static AttributeSyntax? FindAttributeByName(MemberDeclarationSyntax member, string attributeShortName) {
- foreach (var attributeList in member.AttributeLists) {
- foreach (var attribute in attributeList.Attributes) {
- var name = attribute.Name.ToString();
- if (name == attributeShortName || name.EndsWithOrdinal("." + attributeShortName) ||
- name == attributeShortName + "Attribute" ||
- name.EndsWithOrdinal("." + attributeShortName + "Attribute")) {
- return attribute;
- }
- }
- }
-
- return null;
- }
-
- private static Task RemoveAttributeAsync(Document document, SyntaxNode root, AttributeSyntax attribute) {
- var newRoot = RemoveAttribute(root, attribute);
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-
- private static SyntaxNode RemoveAttribute(SyntaxNode root, AttributeSyntax attribute) {
- if (attribute.Parent is not AttributeListSyntax attributeList) {
- return root;
- }
-
- // If this is the only attribute in the list, remove the entire list
- if (attributeList.Attributes.Count == 1) {
- return root.RemoveNode(attributeList, SyntaxRemoveOptions.KeepNoTrivia)!;
- }
-
- // Otherwise, remove just this attribute from the list
- var newList = attributeList.WithAttributes(attributeList.Attributes.Remove(attribute));
- return root.ReplaceNode(attributeList, newList);
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0109NonInterceptableTracedCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0109NonInterceptableTracedCodeFixProvider.cs
deleted file mode 100644
index 9401470..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0109NonInterceptableTracedCodeFixProvider.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0109: Removes [Traced] attribute from non-interceptable methods.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0109NonInterceptableTracedCodeFixProvider))]
-[Shared]
-public sealed partial class Al0109NonInterceptableTracedCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0109NonInterceptableTracedAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
- var node = root.FindNode(diagnostic.Location.SourceSpan);
-
- if (node.AncestorsAndSelf().OfType().FirstOrDefault() is not { } method) {
- return;
- }
-
- if (FindAttributeByName(method, "Traced") is not { } attributeSyntax) {
- return;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0109CodeFixTitle,
- _ => RemoveAttributeAsync(context.Document, root, attributeSyntax),
- nameof(Al0109NonInterceptableTracedCodeFixProvider)),
- diagnostic);
- }
-
- private static AttributeSyntax? FindAttributeByName(MemberDeclarationSyntax member, string attributeShortName) {
- foreach (var attributeList in member.AttributeLists) {
- foreach (var attribute in attributeList.Attributes) {
- var name = attribute.Name.ToString();
- if (name == attributeShortName || name.EndsWithOrdinal("." + attributeShortName) ||
- name == attributeShortName + "Attribute" ||
- name.EndsWithOrdinal("." + attributeShortName + "Attribute")) {
- return attribute;
- }
- }
- }
-
- return null;
- }
-
- private static Task RemoveAttributeAsync(Document document, SyntaxNode root, AttributeSyntax attribute) {
- var newRoot = RemoveAttribute(root, attribute);
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-
- private static SyntaxNode RemoveAttribute(SyntaxNode root, AttributeSyntax attribute) {
- if (attribute.Parent is not AttributeListSyntax attributeList) {
- return root;
- }
-
- // If this is the only attribute in the list, remove the entire list
- if (attributeList.Attributes.Count == 1) {
- return root.RemoveNode(attributeList, SyntaxRemoveOptions.KeepNoTrivia)!;
- }
-
- // Otherwise, remove just this attribute from the list
- var newList = attributeList.WithAttributes(attributeList.Attributes.Remove(attribute));
- return root.ReplaceNode(attributeList, newList);
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0110TracedTagOnOutRefCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0110TracedTagOnOutRefCodeFixProvider.cs
deleted file mode 100644
index 39d3bd6..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0110TracedTagOnOutRefCodeFixProvider.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0110: Removes [TracedTag] attribute from out/ref parameters.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0110TracedTagOnOutRefCodeFixProvider))]
-[Shared]
-public sealed partial class Al0110TracedTagOnOutRefCodeFixProvider : CodeFixProvider {
- /// Gets the diagnostic IDs this provider can fix.
- public override ImmutableArray FixableDiagnosticIds => [Al0110TracedTagOnOutRefParameterAnalyzer.DiagnosticId];
-
- /// Gets the FixAll provider for batch fixing.
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- /// Registers code fixes for the given context.
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root) {
- return;
- }
-
- var diagnostic = context.Diagnostics[0];
- var node = root.FindNode(diagnostic.Location.SourceSpan);
-
- if (node is not ParameterSyntax parameter) {
- return;
- }
-
- if (FindAttributeByName(parameter, "TracedTag") is not { } attributeSyntax) {
- return;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0110CodeFixTitle,
- _ => RemoveAttributeAsync(context.Document, root, attributeSyntax),
- nameof(Al0110TracedTagOnOutRefCodeFixProvider)),
- diagnostic);
- }
-
- private static AttributeSyntax? FindAttributeByName(SyntaxNode node, string attributeShortName) {
- foreach (var attributeList in node.ChildNodes().OfType()) {
- foreach (var attribute in attributeList.Attributes) {
- var name = attribute.Name.ToString();
- if (name == attributeShortName || name.EndsWithOrdinal("." + attributeShortName) ||
- name == attributeShortName + "Attribute" ||
- name.EndsWithOrdinal("." + attributeShortName + "Attribute")) {
- return attribute;
- }
- }
- }
-
- return null;
- }
-
- private static Task RemoveAttributeAsync(Document document, SyntaxNode root, AttributeSyntax attribute) {
- var newRoot = RemoveAttribute(root, attribute);
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-
- private static SyntaxNode RemoveAttribute(SyntaxNode root, AttributeSyntax attribute) {
- if (attribute.Parent is not AttributeListSyntax attributeList) {
- return root;
- }
-
- // If this is the only attribute in the list, remove the entire list
- if (attributeList.Attributes.Count == 1) {
- return root.RemoveNode(attributeList, SyntaxRemoveOptions.KeepNoTrivia)!;
- }
-
- // Otherwise, remove just this attribute from the list
- var newList = attributeList.WithAttributes(attributeList.Attributes.Remove(attribute));
- return root.ReplaceNode(attributeList, newList);
- }
-}
diff --git a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0124AgentTracedCodeFixProvider.cs b/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0124AgentTracedCodeFixProvider.cs
deleted file mode 100644
index 0caed7e..0000000
--- a/src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0124AgentTracedCodeFixProvider.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-
-namespace ANcpLua.Analyzers.CodeFixes.CodeFixes;
-
-///
-/// Code fix provider for AL0124 - removes [AgentTraced] attribute from non-interceptable methods.
-///
-[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Al0124AgentTracedCodeFixProvider))]
-[Shared]
-public sealed partial class Al0124AgentTracedCodeFixProvider : CodeFixProvider {
- public override ImmutableArray FixableDiagnosticIds =>
- [Al0124NonInterceptableAgentTracedAnalyzer.DiagnosticId];
-
- public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
-
- public override async Task RegisterCodeFixesAsync(CodeFixContext context) {
- if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not
- { } root) {
- return;
- }
-
- foreach (var diagnostic in context.Diagnostics) {
- var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
- if (token.Parent?.FirstAncestorOrSelf() is not { } methodDeclaration) {
- continue;
- }
-
- context.RegisterCodeFix(
- CodeAction.Create(
- CodeFixResources.AL0124CodeFixTitle,
- ct => RemoveAgentTracedAttributeAsync(context.Document, methodDeclaration, root, ct),
- nameof(CodeFixResources.AL0124CodeFixTitle)),
- diagnostic);
- }
- }
-
- private static Task RemoveAgentTracedAttributeAsync(
- Document document,
- MethodDeclarationSyntax methodDeclaration,
- SyntaxNode root,
- CancellationToken _) {
- AttributeSyntax? targetAttribute = null;
- AttributeListSyntax? targetList = null;
-
- foreach (var attrList in methodDeclaration.AttributeLists) {
- foreach (var attr in attrList.Attributes) {
- var name = attr.Name.ToString();
- if (name is "AgentTraced" or "AgentTracedAttribute"
- or "Qyl.Instrumentation.Instrumentation.AgentTraced"
- or "Qyl.Instrumentation.Instrumentation.AgentTracedAttribute") {
- targetAttribute = attr;
- targetList = attrList;
- break;
- }
- }
-
- if (targetAttribute is not null) {
- break;
- }
- }
-
- if (targetAttribute is null || targetList is null) {
- return Task.FromResult(document);
- }
-
- SyntaxNode newRoot;
- if (targetList.Attributes.Count == 1) {
- newRoot = root.RemoveNode(targetList, SyntaxRemoveOptions.KeepNoTrivia)!;
- } else {
- newRoot = root.RemoveNode(targetAttribute, SyntaxRemoveOptions.KeepNoTrivia)!;
- }
-
- return Task.FromResult(document.WithSyntaxRoot(newRoot));
- }
-}
diff --git a/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj b/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj
index 262df82..2d97296 100644
--- a/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj
+++ b/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj
@@ -18,7 +18,7 @@
true
en
README.md
- 106 Roslyn diagnostics for modern C# — async pitfalls, OpenTelemetry conventions, AOT safety, ASP.NET Core patterns, and more. Includes 42 automatic code fixes.
+ 88 Roslyn diagnostics for modern C# — async pitfalls, AOT safety, ASP.NET Core patterns, reliability, Roslyn utilities, GenAI tool governance, and version management. Includes 36 automatic code fixes.
false
diff --git a/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md b/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md
index 3a75242..4dcf644 100644
--- a/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md
@@ -15,7 +15,6 @@ AL0007 | Usage | Error | Al0007ToAl0009IXmlSerializableAnalyzer
AL0008 | Usage | Error | Al0007ToAl0009IXmlSerializableAnalyzer
AL0009 | Usage | Error | Al0007ToAl0009IXmlSerializableAnalyzer
AL0011 | Threading | Warning | Al0011LockKeywordAnalyzer
-AL0013 | OpenTelemetry | Info | Al0013MissingSchemaUrlAnalyzer
AL0014 | Style | Warning | Al0014PreferPatternMatchingAnalyzer
AL0015 | Style | Info | Al0015NormalizeNullGuardStyleAnalyzer
AL0016 | Style | Info | Al0016CombineDeclarationWithNullCheckAnalyzer
@@ -62,53 +61,20 @@ AL0057 | Threading | Warning | Al0057ToAl0060ThreadingAnalyzer
AL0058 | Threading | Warning | Al0057ToAl0060ThreadingAnalyzer
AL0059 | Threading | Warning | Al0057ToAl0060ThreadingAnalyzer
AL0060 | Threading | Warning | Al0057ToAl0060ThreadingAnalyzer
-AL0061 | OpenTelemetry | Warning | Al0061ActivityMissingSemconvAnalyzer
-AL0063 | OpenTelemetry | Warning | Al0063UnregisteredActivitySourceAnalyzer
-AL0064 | GenAI | Warning | Al0064GenAiMissingRequiredAttributesAnalyzer
-AL0065 | GenAI | Warning | Al0065UseTokenUsageHistogramAnalyzer
-AL0066 | GenAI | Warning | Al0066InvalidGenAiOperationNameAnalyzer
-AL0067 | Metrics | Warning | Al0067UnregisteredMeterAnalyzer
-AL0068 | Metrics | Warning | Al0068InvalidMetricNameAnalyzer
-AL0069 | Configuration | Warning | Al0069IncompleteServiceDefaultsAnalyzer
-AL0070 | Configuration | Warning | Al0070NonOtlpCollectorEndpointAnalyzer
-AL0071 | Metrics | Error | Al0071MeterClassMustBePartialStaticAnalyzer
-AL0072 | Metrics | Error | Al0072MetricMethodMustBePartialAnalyzer
-AL0073 | OpenTelemetry | Error | Al0073TracedActivitySourceNameAnalyzer
-AL0074 | GenAI | Warning | Al0074DeprecatedGenAiAttributeAnalyzer
-AL0075 | Metrics | Warning | Al0075HighCardinalityMetricTagAnalyzer
-AL0076 | OpenTelemetry | Warning | Al0076MissingOTelConfigurationAnalyzer
-AL0077 | OpenTelemetry | Warning | Al0077DuplicateInstrumentationAnalyzer
-AL0078 | OpenTelemetry | Error | Al0078InvalidActivitySourceNameAnalyzer
-AL0079 | OpenTelemetry | Info | Al0079ManualSpanRecommendedAnalyzer
AL0080 | ASP.NET Core | Warning | Al0080MissingResilienceConfigurationAnalyzer
AL0081 | ASP.NET Core | Warning | Al0081MissingHealthChecksAnalyzer
AL0082 | Configuration | Info | Al0082ConsiderConnectionStringAnalyzer
-AL0083 | Configuration | Warning | Al0083InsecureEndpointAnalyzer
AL0084 | ASP.NET Core | Warning | Al0084MissingServiceDiscoveryAnalyzer
-AL0085 | OpenTelemetry | Error | Al0085InvalidAttributeValueAnalyzer
-AL0086 | OpenTelemetry | Warning | Al0086IncorrectAttributeTypeAnalyzer
-AL0088 | OpenTelemetry | Warning | Al0088SensitiveDataInAttributeAnalyzer
-AL0089 | OpenTelemetry | Warning | Al0089MissingOtlpConfigurationAnalyzer
-AL0090 | OpenTelemetry | Warning | Al0090UncompressedExportAnalyzer
-AL0091 | OpenTelemetry | Warning | Al0091BatchExportDisabledAnalyzer
-AL0092 | OpenTelemetry | Info | Al0092ConsiderSamplingAnalyzer
-AL0093 | OpenTelemetry | Warning | Al0093MissingResourceAttributesAnalyzer
AL0094 | AOT Testing | Warning | Al0094AvoidDynamicKeywordAnalyzer
AL0095 | AOT Testing | Warning | Al0095AvoidExpressionCompileAnalyzer
-AL0096 | Configuration | Warning | Al0096EnableEventSourceSupportAnalyzer
AL0101 | AOT Testing | Warning | Al0101AvoidActivatorCreateInstanceAnalyzer
AL0102 | AOT Testing | Warning | Al0102AvoidTypeGetTypeAnalyzer
AL0103 | Design | Warning | Al0103ClosedTypeHierarchySwitchAnalyzer
AL0104 | Reliability | Warning | Al0104PreferAwaitUsingAnalyzer
AL0105 | Threading | Warning | Al0105AvoidBlockingCallsInAsyncAnalyzer
AL0106 | ASP.NET Core | Warning | Al0106AvoidTaskRunInAspNetCoreAnalyzer
-AL0107 | OpenTelemetry | Warning | Al0107OrphanedTracedTagAnalyzer
-AL0108 | OpenTelemetry | Info | Al0108RedundantNoTraceAnalyzer
-AL0109 | OpenTelemetry | Warning | Al0109NonInterceptableTracedAnalyzer
-AL0110 | OpenTelemetry | Error | Al0110TracedTagOnOutRefParameterAnalyzer
AL0111 | Reliability | Warning | Al0111SqlInterpolationInCommandTextAnalyzer
AL0112 | Reliability | Warning | Al0112FireAndForgetTaskAnalyzer
-AL0113 | OpenTelemetry | Warning | Al0113MissingExceptionRecordingOnActivityAnalyzer
AL0114 | Reliability | Warning | Al0114PreferTryParseAnalyzer
AL0115 | Reliability | Warning | Al0115EmptyCatchBlockAnalyzer
AL0116 | Reliability | Warning | Al0116ExceptionLeakedInResponseAnalyzer
@@ -119,14 +85,11 @@ AL0120 | Roslyn Utilities | Warning | Al0120UseIncrementalGeneratorAnalyzer
AL0121 | Roslyn Utilities | Warning | Al0121NormalizeWhitespaceAnalyzer
AL0122 | Design | Error | Al0122DuckDbTableMustBePartialAnalyzer
AL0123 | Design | Warning | Al0123DuckDbColumnConflictingOrdinalAnalyzer
-AL0124 | GenAI | Warning | Al0124NonInterceptableAgentTracedAnalyzer
AL0125 | Roslyn Utilities | Info | Al0125UseStringComparisonAnyExtensionsAnalyzer
AL0126 | Reliability | Info | Al0126CancellationTokenPropagationAnalyzer
AL0127 | VersionManagement | Warning | Al0127OutdatedMafPackageVersionAnalyzer
AL0128 | GenAI | Warning | Al0128DestructiveToolMustRequireApprovalAnalyzer
AL0129 | GenAI | Info | Al0129ToolMustDeclareSideEffectAnalyzer
AL0130 | GenAI | Info | Al0130ToolMustDeclareCapabilityAnalyzer
-AL0131 | GenAI | Warning | Al0131DirectGenAiSdkUsageAnalyzer
-AL0135 | OpenTelemetry | Warning | Al0135LegacySemanticConventionsAccessorAnalyzer
AL0137 | Roslyn Utilities | Warning | Al0137UseGuardForThrowIfAnalyzer
AL0138 | Reliability | Warning | Al0138UseExplicitMidpointRoundingAnalyzer
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0013MissingSchemaUrlAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0013MissingSchemaUrlAnalyzer.cs
deleted file mode 100644
index 146397e..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0013MissingSchemaUrlAnalyzer.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0013: Detects OpenTelemetry configurations that don't set the schema URL.
-///
-///
-///
-/// The OpenTelemetry specification recommends setting a schema URL on resources
-/// to indicate which version of the semantic conventions is being used. Without
-/// a schema URL, telemetry backends cannot automatically transform attributes
-/// between convention versions, potentially causing data inconsistencies.
-///
-///
-/// The analyzer flags resource configuration calls like ConfigureResource,
-/// SetResourceBuilder, and AddResource on OpenTelemetry builder
-/// types when they do not include a schema URL reference.
-///
-///
-/// Schema URL detection is heuristic: the analyzer looks for string literals
-/// containing "schema", "telemetry.schema_url", or "opentelemetry.io/schemas",
-/// as well as method calls with "Schema" in the name. This may not catch all
-/// indirect configurations but covers common patterns.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0013MissingSchemaUrlAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0013.
- private const string DiagnosticId = "AL0013";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverity.Info);
-
- /// Set of method names that configure OTel resources.
- private static readonly HashSet s_resourceConfigMethods = [
- "ConfigureResource",
- "SetResourceBuilder",
- "AddResource",
- "WithResource",
- "ConfigureOpenTelemetry"
- ];
-
- /// Array of known OTel builder type names to check for resource configuration.
- private static readonly string[] s_otelBuilderTypeNames = [
- "OpenTelemetry.Trace.TracerProviderBuilder",
- "OpenTelemetry.Metrics.MeterProviderBuilder",
- "OpenTelemetry.Logs.LoggerProviderBuilder",
- "OpenTelemetry.OpenTelemetryBuilder",
- "OpenTelemetry.IOpenTelemetryBuilder"
- ];
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers compilation start action to analyze OTel resource configurations.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var otelBuilderTypes = s_otelBuilderTypeNames
- .Select(context.Compilation.GetTypeByMetadataName)
- .WhereNotNull()
- .ToImmutableArray();
-
- if (otelBuilderTypes.IsEmpty) {
- return;
- }
-
- context.RegisterSyntaxNodeAction(
- ctx => AnalyzeInvocation(ctx, otelBuilderTypes),
- SyntaxKind.InvocationExpression);
- }
-
- private static void AnalyzeInvocation(
- SyntaxNodeAnalysisContext context,
- ImmutableArray otelBuilderTypes) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- var methodName = GetMethodName(invocation);
- if (methodName is null || !s_resourceConfigMethods.Contains(methodName)) {
- return;
- }
-
- if (!IsOtelBuilderCall(invocation, context.SemanticModel, otelBuilderTypes, context.CancellationToken)) {
- return;
- }
-
- if (CheckForSchemaUrl(invocation)) {
- return;
- }
-
- var location = GetMethodLocation(invocation);
- context.ReportDiagnostic(s_rule, location);
- }
-
- private static bool IsOtelBuilderCall(
- InvocationExpressionSyntax invocation,
- SemanticModel semanticModel,
- ImmutableArray otelBuilderTypes,
- CancellationToken cancellationToken) {
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) {
- return false;
- }
-
- if (ModelExtensions.GetTypeInfo(semanticModel, memberAccess.Expression, cancellationToken).Type is not { } receiverType) {
- return false;
- }
-
- // Check if receiver type inherits from or implements any OTel builder type
- return otelBuilderTypes.Any(builderType =>
- receiverType.InheritsFrom(builderType) || receiverType.Implements(builderType));
- }
-
- private static bool CheckForSchemaUrl(SyntaxNode invocation) {
- foreach (var node in invocation.DescendantNodes()) {
- switch (node) {
- case LiteralExpressionSyntax literal: {
- var value = literal.Token.ValueText;
- if (value.ContainsIgnoreCase("schema") ||
- value.ContainsIgnoreCase("telemetry.schema_url") ||
- value.ContainsIgnoreCase("opentelemetry.io/schemas")) {
- return true;
- }
-
- break;
- }
- case InvocationExpressionSyntax nestedInvocation: {
- var nestedMethod = GetMethodName(nestedInvocation);
- if (nestedMethod?.ContainsIgnoreCase("Schema") == true) {
- return true;
- }
-
- break;
- }
- }
- }
-
- return false;
- }
-
- private static Location GetMethodLocation(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.GetLocation(),
- IdentifierNameSyntax identifier => identifier.GetLocation(),
- _ => invocation.GetLocation()
- };
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0061ActivityMissingSemconvAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0061ActivityMissingSemconvAnalyzer.cs
deleted file mode 100644
index 1688793..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0061ActivityMissingSemconvAnalyzer.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0061: Detects Activity/Span creation without semantic convention attributes.
-///
-///
-///
-/// OpenTelemetry Activities (Spans) should include semantic convention attributes
-/// appropriate for their operation type to enable correlation, filtering, and analysis.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0061ActivityMissingSemconvAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0061.
- private const string DiagnosticId = "AL0061";
-
- // Operation types and their expected semantic convention prefixes
- private static readonly Dictionary s_operationTypePrefixes = new(StringComparer.OrdinalIgnoreCase) {
- ["http"] = ["http.", "url.", "server.", "client.", "network.", "user_agent."],
- ["db"] = ["db."],
- ["rpc"] = ["rpc.", "jsonrpc."],
- ["messaging"] = ["messaging."],
- ["faas"] = ["faas."],
- ["gen_ai"] = ["gen_ai.", "openai."]
- };
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze Activity.StartActivity calls.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
-
- private static void AnalyzeInvocation(OperationAnalysisContext context) {
- var invocation = (IInvocationOperation)context.Operation;
-
- if (invocation.TargetMethod.Name != "StartActivity" ||
- GetActivityName(invocation) is not { } activityName ||
- InferOperationType(activityName) is not { } operationType ||
- !s_operationTypePrefixes.TryGetValue(operationType, out var expectedPrefixes)) {
- return;
- }
-
- var setTags = CollectSetTagCalls(invocation);
- if (!HasRelevantSemconv(setTags, expectedPrefixes)) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, invocation.Syntax.GetLocation(), activityName, operationType));
- }
- }
-
- private static bool HasRelevantSemconv(HashSet tagNames, string[] expectedPrefixes) {
- foreach (var tagName in tagNames) {
- if (expectedPrefixes.Any(tagName.StartsWithIgnoreCase)) {
- return true;
- }
-
- if (OpenTelemetryDeprecatedSemconvCatalog.TryGetDeprecatedAttribute(tagName, out var deprecatedAttribute) &&
- expectedPrefixes.Any(deprecatedAttribute.Replacement.StartsWithIgnoreCase)) {
- return true;
- }
-
- if (OpenTelemetryDeprecatedSemconvCatalog.TryGetDeprecatedGenAiAttribute(tagName, out var genAiReplacement) &&
- expectedPrefixes.Any(genAiReplacement.StartsWithIgnoreCase)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static string? GetActivityName(IInvocationOperation invocation) =>
- invocation.Arguments.Length > 0 &&
- invocation.Arguments[0].Value.ConstantValue is { HasValue: true, Value: string name }
- ? name
- : null;
-
- private static string? InferOperationType(string activityName) {
- foreach (var kvp in s_operationTypePrefixes) {
- if (activityName.ContainsIgnoreCase(kvp.Key)) {
- return kvp.Key;
- }
- }
-
- // Additional heuristics
- if (activityName.ContainsIgnoreCase("request") ||
- activityName.ContainsIgnoreCase("response") ||
- activityName.ContainsIgnoreCase("get") ||
- activityName.ContainsIgnoreCase("post")) {
- return "http";
- }
-
- if (activityName.ContainsIgnoreCase("query") ||
- activityName.ContainsIgnoreCase("execute") ||
- activityName.ContainsIgnoreCase("select") ||
- activityName.ContainsIgnoreCase("insert")) {
- return "db";
- }
-
- if (activityName.ContainsIgnoreCase("chat") ||
- activityName.ContainsIgnoreCase("completion") ||
- activityName.ContainsIgnoreCase("embedding") ||
- activityName.ContainsIgnoreCase("llm")) {
- return "gen_ai";
- }
-
- return null;
- }
-
- private static HashSet CollectSetTagCalls(IInvocationOperation startActivity) {
- var tags = new HashSet(StringComparer.OrdinalIgnoreCase);
-
- for (var current = startActivity.Parent; current is not null; current = current.Parent) {
- if (current is IBlockOperation block) {
- CollectSetTagCallsRecursive(block, tags);
- break;
- }
- }
-
- return tags;
- }
-
- private static void CollectSetTagCallsRecursive(IOperation operation, HashSet tags) {
- if (operation is IInvocationOperation { TargetMethod.Name: "SetTag", Arguments.Length: >= 1 } invocation &&
- invocation.Arguments[0].Value.ConstantValue is { HasValue: true, Value: string tagName }) {
- tags.Add(tagName);
- return;
- }
-
- foreach (var child in operation.ChildOperations) {
- CollectSetTagCallsRecursive(child, tags);
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0063UnregisteredActivitySourceAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0063UnregisteredActivitySourceAnalyzer.cs
deleted file mode 100644
index 3691353..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0063UnregisteredActivitySourceAnalyzer.cs
+++ /dev/null
@@ -1,183 +0,0 @@
-using System.Collections.Concurrent;
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0063: Detects ActivitySource instances that are not registered with AddSource().
-///
-///
-///
-/// ActivitySources must be registered with AddSource() in the OpenTelemetry tracing
-/// configuration to emit spans. Unregistered sources will silently fail to produce traces.
-///
-///
-/// Uses cross-compilation analysis: collects all AddSource() calls and ActivitySource
-/// creations across the entire compilation, then reports only unmatched sources.
-/// Supports wildcard patterns (e.g., AddSource("OpenAI.*") covers "OpenAI.Chat").
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0063UnregisteredActivitySourceAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0063.
- private const string DiagnosticId = "AL0063";
-
- private const string ActivitySourceTypeName = "System.Diagnostics.ActivitySource";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers compilation-wide analysis for cross-file ActivitySource tracking.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- if (context.Compilation.GetTypeByMetadataName(ActivitySourceTypeName) is not { } activitySourceSymbol) {
- return;
- }
-
- var tracerProviderBuilderType = context.Compilation.GetTypeByMetadataName("OpenTelemetry.Trace.TracerProviderBuilder");
-
- var registeredSources = new ConcurrentBag();
- var activitySourceCreations = new ConcurrentBag<(Location Location, string Name)>();
- var fieldSourceNames = new ConcurrentDictionary>(SymbolEqualityComparer.Default);
- var pendingForeachResolutions = new ConcurrentBag();
-
- // Pre-index static readonly string[] fields once, using the already-provided ctx.SemanticModel
- context.RegisterSyntaxNodeAction(
- ctx => CollectStaticReadonlyFieldConstants(ctx, fieldSourceNames),
- SyntaxKind.VariableDeclarator);
-
- context.RegisterOperationAction(ctx => {
- var invocation = (IInvocationOperation)ctx.Operation;
- if (!invocation.IsMethodNamed(tracerProviderBuilderType, "AddSource") || invocation.Arguments.Length is 0) {
- return;
- }
-
- var argumentValue = invocation.Arguments[0].Value;
-
- if (argumentValue.ConstantValue is { HasValue: true, Value: string name }) {
- registeredSources.Add(name);
- } else {
- // Defer foreach resolution to CompilationEnd where fieldSourceNames is fully populated
- pendingForeachResolutions.Add(argumentValue);
- }
- }, OperationKind.Invocation);
-
- context.RegisterOperationAction(ctx => {
- var creation = (IObjectCreationOperation)ctx.Operation;
- if (!creation.Type.IsEqualTo(activitySourceSymbol) || creation.Arguments.Length is 0) {
- return;
- }
-
- if (creation.Arguments[0].Value.ConstantValue is { HasValue: true, Value: string name }) {
- activitySourceCreations.Add((creation.Arguments[0].Value.Syntax.GetLocation(), name));
- }
- }, OperationKind.ObjectCreation);
-
- context.RegisterCompilationEndAction(endCtx => {
- // Resolve foreach-based registrations now that all field constants are indexed
- foreach (var argumentValue in pendingForeachResolutions) {
- ResolveFromForeachCollection(argumentValue, fieldSourceNames, registeredSources);
- }
-
- var registered = registeredSources.ToArray();
-
- foreach (var (location, sourceName) in activitySourceCreations) {
- if (!IsRegistered(sourceName, registered)) {
- endCtx.ReportDiagnostic(Diagnostic.Create(s_rule, location, sourceName));
- }
- }
- });
- }
-
- private static void CollectStaticReadonlyFieldConstants(
- SyntaxNodeAnalysisContext context,
- ConcurrentDictionary> fieldSourceNames) {
- var declarator = (VariableDeclaratorSyntax)context.Node;
-
- if (declarator.Initializer?.Value is not { } initializerValue) {
- return;
- }
-
- if (context.SemanticModel.GetDeclaredSymbol(declarator, context.CancellationToken)
- is not IFieldSymbol { IsStatic: true, IsReadOnly: true } field) {
- return;
- }
-
- if (GetInitializerElements(initializerValue) is not { } elements) {
- return;
- }
-
- var builder = ImmutableArray.CreateBuilder();
-
- foreach (var expr in elements) {
- if (context.SemanticModel.GetConstantValue(expr, context.CancellationToken)
- is { HasValue: true, Value: string name }) {
- builder.Add(name);
- }
- }
-
- if (builder.Count > 0) {
- fieldSourceNames.TryAdd(field, builder.ToImmutable());
- }
- }
-
- private static bool IsRegistered(string sourceName, ReadOnlySpan registered) {
- foreach (var pattern in registered) {
- if (string.Equals(pattern, sourceName, StringComparison.Ordinal)) {
- return true;
- }
-
- // Wildcard: "OpenAI.*" matches "OpenAI.Chat", "OpenAI.Embeddings", etc.
- if (pattern.Length > 2 &&
- pattern.EndsWithOrdinal(".*") &&
- sourceName.AsSpan().StartsWith(pattern.AsSpan(0, pattern.Length - 2), StringComparison.Ordinal)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static void ResolveFromForeachCollection(
- IOperation argumentValue,
- ConcurrentDictionary> fieldSourceNames,
- ConcurrentBag registeredSources) {
- var current = argumentValue.Parent;
- while (current is not null and not IForEachLoopOperation) {
- current = current.Parent;
- }
-
- if (current is not IForEachLoopOperation foreachLoop) {
- return;
- }
-
- var collection = foreachLoop.Collection.UnwrapAllConversions();
-
- if (collection is not IFieldReferenceOperation { Field: var field }) {
- return;
- }
-
- if (!fieldSourceNames.TryGetValue(field, out var names)) {
- return;
- }
-
- foreach (var name in names.AsSpan()) {
- registeredSources.Add(name);
- }
- }
-
- private static IEnumerable? GetInitializerElements(ExpressionSyntax initializer) =>
- initializer switch {
- CollectionExpressionSyntax col => col.Elements.OfType()
- .Select(static e => e.Expression),
- ArrayCreationExpressionSyntax arr => arr.Initializer?.Expressions,
- ImplicitArrayCreationExpressionSyntax implArr => implArr.Initializer.Expressions,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0064GenAiMissingRequiredAttributesAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0064GenAiMissingRequiredAttributesAnalyzer.cs
deleted file mode 100644
index ad1897a..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0064GenAiMissingRequiredAttributesAnalyzer.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0064: Detects GenAI spans that are missing required semantic convention attributes.
-///
-///
-///
-/// GenAI spans require these attributes for proper observability:
-///
-/// - gen_ai.provider.name - The GenAI provider (e.g., "openai")
-/// - gen_ai.request.model - The model name
-/// - gen_ai.operation.name - The operation type
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0064GenAiMissingRequiredAttributesAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0064.
- private const string DiagnosticId = "AL0064";
-
- private static readonly string[] s_requiredGenAiAttributes = OpenTelemetryGenAiSemconvFacts.s_requiredAttributeKeys;
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.GenAI,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze Activity.StartActivity calls.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
-
- private static void AnalyzeInvocation(OperationAnalysisContext context) {
- var invocation = (IInvocationOperation)context.Operation;
-
- if (invocation.TargetMethod.Name != "StartActivity" ||
- GetActivityName(invocation) is not { } activityName ||
- !IsGenAiActivity(activityName)) {
- return;
- }
-
- var setTags = CollectSetTagCalls(invocation);
-
- foreach (var requiredAttribute in s_requiredGenAiAttributes) {
- if (!setTags.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase)) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, invocation.Syntax.GetLocation(), activityName, requiredAttribute));
- }
- }
- }
-
- private static string? GetActivityName(IInvocationOperation invocation) =>
- invocation.Arguments.Length > 0 &&
- invocation.Arguments[0].Value.ConstantValue is { HasValue: true, Value: string name }
- ? name
- : null;
-
- private static bool IsGenAiActivity(string activityName) =>
- activityName.ContainsIgnoreCase("gen_ai") ||
- activityName.ContainsIgnoreCase("genai") ||
- activityName.ContainsIgnoreCase("chat") ||
- activityName.ContainsIgnoreCase("completion") ||
- activityName.ContainsIgnoreCase("embedding");
-
- private static HashSet CollectSetTagCalls(IInvocationOperation startActivity) {
- var tags = new HashSet(StringComparer.OrdinalIgnoreCase);
-
- for (var current = startActivity.Parent; current is not null; current = current.Parent) {
- if (current is IBlockOperation block) {
- CollectSetTagCallsRecursive(block, tags);
- break;
- }
- }
-
- return tags;
- }
-
- private static void CollectSetTagCallsRecursive(IOperation operation, HashSet tags) {
- if (operation is IInvocationOperation { TargetMethod.Name: "SetTag", Arguments.Length: >= 1 } invocation &&
- invocation.Arguments[0].Value.ConstantValue is { HasValue: true, Value: string tagName }) {
- tags.Add(tagName);
- return;
- }
-
- foreach (var child in operation.ChildOperations) {
- CollectSetTagCallsRecursive(child, tags);
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0065UseTokenUsageHistogramAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0065UseTokenUsageHistogramAnalyzer.cs
deleted file mode 100644
index bb4bf97..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0065UseTokenUsageHistogramAnalyzer.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0065: Detects token usage metrics that don't use the standard histogram.
-///
-///
-///
-/// GenAI token usage should be recorded using the standard histogram:
-/// gen_ai.client.token.usage
-///
-///
-/// This ensures compatibility with standard GenAI observability dashboards.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0065UseTokenUsageHistogramAnalyzer : AlAnalyzer {
- private const string CorrectMetricName = "gen_ai.client.token.usage";
- private const string HistogramAttributeFullName = "Qyl.Instrumentation.Instrumentation.HistogramAttribute";
-
- private static readonly string[] s_tokenRelatedPatterns = [
- "token", "input_token", "output_token", "prompt_token", "completion_token"
- ];
-
- /// The diagnostic identifier for AL0065.
- private const string DiagnosticId = "AL0065";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.GenAI,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze methods with histogram attributes.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
-
- private static void AnalyzeMethod(SymbolAnalysisContext context) {
- var method = (IMethodSymbol)context.Symbol;
-
- if (context.Compilation.GetTypeByMetadataName(HistogramAttributeFullName) is not { } histogramType) {
- return;
- }
-
- foreach (var attribute in method.GetAttributes()) {
- if (!attribute.AttributeClass.IsEqualTo(histogramType) ||
- attribute.ConstructorArguments.Length is 0 ||
- attribute.ConstructorArguments[0].Value is not string metricName ||
- !IsTokenRelatedMetric(metricName) ||
- metricName.EqualsIgnoreCase(CorrectMetricName)) {
- continue;
- }
-
- var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)
- .GetLocation() ?? method.Locations.FirstOrDefault();
-
- if (location is not null) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, location, metricName));
- }
- }
- }
-
- private static bool IsTokenRelatedMetric(string metricName) =>
- s_tokenRelatedPatterns.Any(metricName.ContainsIgnoreCase);
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0066InvalidGenAiOperationNameAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0066InvalidGenAiOperationNameAnalyzer.cs
deleted file mode 100644
index 862d29e..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0066InvalidGenAiOperationNameAnalyzer.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0066: Detects GenAI operation names that don't follow semantic conventions.
-///
-///
-///
-/// GenAI operation names should be one of the standard values:
-///
-/// - chat - for chat completions
-/// - generate_content - for content generation APIs
-/// - text_completion - for text completions
-/// - embeddings - for embedding generation
-/// - retrieval - for retrieval operations
-/// - create_agent, invoke_agent, execute_tool, invoke_workflow - for agent/workflow operations
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0066InvalidGenAiOperationNameAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0066.
- private const string DiagnosticId = "AL0066";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.GenAI,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze string literals used as operation names.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
-
- private static void AnalyzeInvocation(OperationAnalysisContext context) {
- var invocation = (IInvocationOperation)context.Operation;
-
- if (invocation.TargetMethod.Name != "SetTag" ||
- invocation.Arguments.Length < 2 ||
- invocation.Arguments[0].Value.UnwrapAllConversions().ConstantValue is not { HasValue: true, Value: string tagName } ||
- !tagName.EqualsIgnoreCase("gen_ai.operation.name") ||
- invocation.Arguments[1].Value.UnwrapAllConversions().ConstantValue is not { HasValue: true, Value: string operationName } ||
- OpenTelemetryGenAiSemconvFacts.IsValidOperationName(operationName)) {
- return;
- }
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, invocation.Arguments[1].Syntax.GetLocation(), operationName));
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0067UnregisteredMeterAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0067UnregisteredMeterAnalyzer.cs
deleted file mode 100644
index 1da4ea3..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0067UnregisteredMeterAnalyzer.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0067: Detects Meter instances that are not registered with AddMeter() anywhere in the compilation.
-///
-///
-///
-/// Meters must be registered via MeterProviderBuilder.AddMeter(params string[] names) in the
-/// OpenTelemetry metrics configuration to export metrics. Unregistered meters silently discard data.
-///
-///
-/// The analyzer resolves string arguments through constant propagation, so both literals
-/// (AddMeter("qyl.agent")) and symbolic references (AddMeter(ActivitySources.Agent) where
-/// Agent is a const string) are recognised as registrations.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0067UnregisteredMeterAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0067.
- private const string DiagnosticId = "AL0067";
-
- private const string MeterTypeName = "System.Diagnostics.Metrics.Meter";
- private const string AddMeterMethodName = "AddMeter";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Metrics,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers compilation-wide analysis to correlate Meter creations with AddMeter registrations.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var registered = new ConcurrentDictionary(StringComparer.Ordinal);
- var creations = new ConcurrentBag<(Location Location, string Name)>();
-
- context.RegisterOperationAction(
- ctx => CollectRegistrations((IInvocationOperation)ctx.Operation, registered),
- OperationKind.Invocation);
-
- context.RegisterOperationAction(
- ctx => CollectCreation((IObjectCreationOperation)ctx.Operation, creations),
- OperationKind.ObjectCreation);
-
- context.RegisterCompilationEndAction(end => {
- foreach (var (location, name) in creations) {
- if (!registered.ContainsKey(name)) {
- end.ReportDiagnostic(Diagnostic.Create(s_rule, location, name));
- }
- }
- });
- }
-
- private static void CollectRegistrations(
- IInvocationOperation invocation,
- ConcurrentDictionary registered) {
- if (invocation.TargetMethod.Name is not AddMeterMethodName) {
- return;
- }
-
- foreach (var argument in invocation.Arguments) {
- CollectStringValues(argument.Value, registered);
- }
- }
-
- private static void CollectStringValues(IOperation op, ConcurrentDictionary collector) {
- if (op.ConstantValue is { HasValue: true, Value: string s }) {
- collector.TryAdd(s, 0);
- return;
- }
-
- // params string[] is lowered to an implicit array creation; descend into its elements.
- if (op is IArrayCreationOperation { Initializer: { } initializer }) {
- foreach (var element in initializer.ElementValues) {
- CollectStringValues(element, collector);
- }
- }
- }
-
- private static void CollectCreation(
- IObjectCreationOperation creation,
- ConcurrentBag<(Location Location, string Name)> creations) {
- if (creation.Type?.ToDisplayString() != MeterTypeName ||
- creation.Arguments.Length is 0 ||
- creation.Arguments[0].Value.ConstantValue is not { HasValue: true, Value: string meterName }) {
- return;
- }
-
- creations.Add((creation.Syntax.GetLocation(), meterName));
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0068InvalidMetricNameAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0068InvalidMetricNameAnalyzer.cs
deleted file mode 100644
index a26b058..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0068InvalidMetricNameAnalyzer.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System.Text.RegularExpressions;
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0068: Detects metric instrument names that don't follow naming conventions.
-///
-///
-///
-/// Metric names should follow OpenTelemetry naming conventions:
-///
-/// - Use dot-separated namespaces (e.g., myapp.orders.count)
-/// - Use snake_case for individual words
-/// - Include unit as suffix when applicable
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0068InvalidMetricNameAnalyzer : AlAnalyzer {
- private const string CounterAttributeFullName = "Qyl.Instrumentation.Instrumentation.CounterAttribute";
- private const string HistogramAttributeFullName = "Qyl.Instrumentation.Instrumentation.HistogramAttribute";
-
- // Pattern: lowercase letters, numbers, dots, and underscores only
- // Should have at least one dot (namespace separator)
- private static readonly Regex s_validNamePattern = new(
- @"^[a-z][a-z0-9_.]*\.[a-z][a-z0-9_.]*$",
- RegexOptions.Compiled | RegexOptions.CultureInvariant);
-
- /// The diagnostic identifier for AL0068.
- private const string DiagnosticId = "AL0068";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Metrics,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze methods with metric attributes.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
-
- private static void AnalyzeMethod(SymbolAnalysisContext context) {
- var method = (IMethodSymbol)context.Symbol;
-
- var counterType = context.Compilation.GetTypeByMetadataName(CounterAttributeFullName);
- var histogramType = context.Compilation.GetTypeByMetadataName(HistogramAttributeFullName);
-
- foreach (var attribute in method.GetAttributes()) {
- if (!attribute.AttributeClass.IsEqualTo(counterType) &&
- !attribute.AttributeClass.IsEqualTo(histogramType)) {
- continue;
- }
-
- if (attribute.ConstructorArguments.Length is 0 ||
- attribute.ConstructorArguments[0].Value is not string metricName ||
- string.IsNullOrWhiteSpace(metricName) ||
- s_validNamePattern.IsMatch(metricName)) {
- continue;
- }
-
- var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)
- .GetLocation() ?? method.Locations.FirstOrDefault();
-
- if (location is not null) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, location, metricName));
- }
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0069IncompleteServiceDefaultsAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0069IncompleteServiceDefaultsAnalyzer.cs
deleted file mode 100644
index d3aad03..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0069IncompleteServiceDefaultsAnalyzer.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0069: Detects incomplete ServiceDefaults configuration.
-///
-///
-///
-/// Complete ServiceDefaults configuration should include:
-///
-/// - Tracing configuration (AddTracing/WithTracing)
-/// - Metrics configuration (AddMetrics/WithMetrics)
-/// - Logging configuration (AddLogging/WithLogging)
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0069IncompleteServiceDefaultsAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0069.
- private const string DiagnosticId = "AL0069";
-
- private static readonly string[] s_tracingMethods = ["AddOpenTelemetry", "WithTracing", "AddTracing"];
- private static readonly string[] s_metricsMethods = ["AddOpenTelemetry", "WithMetrics", "AddMetrics"];
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Configuration,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax tree actions to analyze ServiceDefaults configuration.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
-
- private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- // Look for ConfigureOpenTelemetry or AddServiceDefaults calls
- var methodName = GetMethodName(invocation);
- if (methodName is not ("ConfigureOpenTelemetry" or "AddServiceDefaults" or "AddOpenTelemetry")) {
- return;
- }
-
- // Check if this is in a method body (likely configuration code)
- if (invocation.Ancestors().OfType().FirstOrDefault() is not { } containingMethod) {
- return;
- }
-
- // Collect all method invocations in the same method
- var allInvocations = new HashSet();
- foreach (var inv in containingMethod.DescendantNodes().OfType()) {
- var name = GetMethodName(inv);
- if (name is not null) {
- allInvocations.Add(name);
- }
- }
-
- // Check for tracing configuration
- var hasTracing = s_tracingMethods.Any(allInvocations.Contains);
- var hasMetrics = s_metricsMethods.Any(allInvocations.Contains);
-
- // Report missing components
- if (!hasTracing) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- invocation.GetLocation(),
- "tracing",
- "WithTracing() or AddTracing()"));
- }
-
- if (!hasMetrics) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- invocation.GetLocation(),
- "metrics",
- "WithMetrics() or AddMetrics()"));
- }
-
- // Note: Logging is optional, so we don't report it as missing
- }
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0070NonOtlpCollectorEndpointAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0070NonOtlpCollectorEndpointAnalyzer.cs
deleted file mode 100644
index 86c51bb..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0070NonOtlpCollectorEndpointAnalyzer.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0070: Detects collector endpoint configurations that don't use OTLP protocol.
-///
-///
-///
-/// Collector endpoints should use the OTLP protocol for standardized telemetry export:
-///
-/// - gRPC: grpc://host:4317
-/// - HTTP: http://host:4318/v1/traces (or /v1/metrics, /v1/logs)
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0070NonOtlpCollectorEndpointAnalyzer : AlAnalyzer {
- private static readonly string[] s_otlpPatterns = [
- "4317", // gRPC default port
- "4318", // HTTP default port
- "/v1/traces",
- "/v1/metrics",
- "/v1/logs",
- "otlp"
- ];
-
- private static readonly string[] s_endpointPropertyNames = [
- "Endpoint", "CollectorEndpoint", "OtlpEndpoint", "ExporterEndpoint"
- ];
-
- /// The diagnostic identifier for AL0070.
- private const string DiagnosticId = "AL0070";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Configuration,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze endpoint assignments.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment);
-
- private static void AnalyzeAssignment(OperationAnalysisContext context) {
- var assignment = (ISimpleAssignmentOperation)context.Operation;
-
- if (GetPropertyName(assignment.Target) is not { } propertyName ||
- !s_endpointPropertyNames.Contains(propertyName, StringComparer.OrdinalIgnoreCase) ||
- assignment.Value.ConstantValue is not { HasValue: true, Value: string endpoint } ||
- IsOtlpEndpoint(endpoint)) {
- return;
- }
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, assignment.Syntax.GetLocation(), endpoint));
- }
-
- private static string? GetPropertyName(IOperation target) =>
- target switch {
- IPropertyReferenceOperation propRef => propRef.Property.Name,
- IMemberReferenceOperation memberRef => memberRef.Member.Name,
- _ => null
- };
-
- private static bool IsOtlpEndpoint(string endpoint) =>
- s_otlpPatterns.Any(endpoint.ContainsIgnoreCase);
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0071MeterClassMustBePartialStaticAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0071MeterClassMustBePartialStaticAnalyzer.cs
deleted file mode 100644
index 99082b7..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0071MeterClassMustBePartialStaticAnalyzer.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0071: Detects [Meter] classes that are not declared as partial static.
-///
-///
-///
-/// The source generator requires [Meter] classes to be partial static because:
-///
-/// - The generator creates static Meter and instrument fields
-/// - The generator implements partial methods that record metrics
-/// - Static classes ensure single instance of meter/instruments
-///
-///
-///
-/// Example of correct usage:
-///
-/// [Meter("MyApp")]
-/// public static partial class AppMetrics
-/// {
-/// [Counter("orders.created")]
-/// public static partial void RecordOrderCreated([Tag("status")] string status);
-/// }
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0071MeterClassMustBePartialStaticAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0071.
- public const string DiagnosticId = "AL0071";
-
- private const string MeterAttributeFullName = "Qyl.Instrumentation.Instrumentation.MeterAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Metrics,
- DiagnosticSeverities.RequiredFix);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax node actions to analyze class declarations with [Meter] attribute.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
-
- private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) {
- var classDeclaration = (ClassDeclarationSyntax)context.Node;
-
- if (classDeclaration.AttributeLists.Count is 0) {
- return;
- }
-
- if (context.SemanticModel.GetDeclaredSymbol(classDeclaration, context.CancellationToken) is not { } classSymbol
- || !HasMeterAttribute(classSymbol, context.SemanticModel.Compilation)) {
- return;
- }
-
- if (!classDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)
- || !classDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- classDeclaration.Identifier.GetLocation(),
- classSymbol.Name));
- }
- }
-
- private static bool HasMeterAttribute(INamedTypeSymbol classSymbol, Compilation compilation) =>
- compilation.GetTypeByMetadataName(MeterAttributeFullName) is { } meterAttributeType
- && classSymbol.GetAttributes().Any(a => a.AttributeClass.IsEqualTo(meterAttributeType));
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0072MetricMethodMustBePartialAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0072MetricMethodMustBePartialAnalyzer.cs
deleted file mode 100644
index 1a3c009..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0072MetricMethodMustBePartialAnalyzer.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0072: Detects [Counter]/[Histogram] methods that are not declared as partial.
-///
-///
-///
-/// The source generator requires metric methods to be partial because:
-///
-/// - The generator creates the method implementation
-/// - The implementation records values to the appropriate instrument
-/// - Without partial, the method would have no body or conflict with generated code
-///
-///
-///
-/// Example of correct usage:
-///
-/// [Counter("orders.created")]
-/// public static partial void RecordOrderCreated([Tag("status")] string status);
-///
-/// [Histogram("order.processing.duration", Unit = "ms")]
-/// public static partial void RecordProcessingDuration(double duration, [Tag("type")] string orderType);
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0072MetricMethodMustBePartialAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0072.
- public const string DiagnosticId = "AL0072";
-
- private const string CounterAttributeFullName = "Qyl.Instrumentation.Instrumentation.CounterAttribute";
- private const string HistogramAttributeFullName = "Qyl.Instrumentation.Instrumentation.HistogramAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Metrics,
- DiagnosticSeverities.RequiredFix);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax node actions to analyze method declarations with metric attributes.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
-
- private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) {
- var methodDeclaration = (MethodDeclarationSyntax)context.Node;
-
- if (methodDeclaration.AttributeLists.Count is 0) {
- return;
- }
-
- if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken) is not { } methodSymbol
- || GetMetricAttributeName(methodSymbol, context.SemanticModel.Compilation) is not { } metricAttributeName) {
- return;
- }
-
- if (!methodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- methodDeclaration.Identifier.GetLocation(),
- methodSymbol.Name,
- metricAttributeName));
- }
- }
-
- private static string? GetMetricAttributeName(IMethodSymbol methodSymbol, Compilation compilation) {
- var counterAttributeType = compilation.GetTypeByMetadataName(CounterAttributeFullName);
- var histogramAttributeType = compilation.GetTypeByMetadataName(HistogramAttributeFullName);
-
- foreach (var attribute in methodSymbol.GetAttributes()) {
- if (counterAttributeType is not null &&
- attribute.AttributeClass.IsEqualTo(counterAttributeType)) {
- return "Counter";
- }
-
- if (histogramAttributeType is not null &&
- attribute.AttributeClass.IsEqualTo(histogramAttributeType)) {
- return "Histogram";
- }
- }
-
- return null;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0073TracedActivitySourceNameAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0073TracedActivitySourceNameAnalyzer.cs
deleted file mode 100644
index c932b23..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0073TracedActivitySourceNameAnalyzer.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0073: Validates [Traced] attribute has non-empty ActivitySourceName.
-///
-///
-///
-/// The [Traced] attribute requires a valid ActivitySourceName because:
-///
-/// - The source name identifies where spans originate
-/// - It must match a registered ActivitySource in the tracing pipeline
-/// - Empty names prevent proper span correlation and filtering
-///
-///
-///
-/// Example of correct usage:
-///
-/// [Traced("MyApp.Orders")] // Valid: descriptive source name
-/// public class OrderService { }
-///
-/// [Traced("")] // Error: empty source name
-/// public class BadService { }
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0073TracedActivitySourceNameAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0073.
- public const string DiagnosticId = "AL0073";
-
- private const string TracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.RequiredFix);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze types and methods with [Traced] attribute.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
- context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
- }
-
- private static void AnalyzeNamedType(SymbolAnalysisContext context) =>
- AnalyzeSymbol(context, (INamedTypeSymbol)context.Symbol);
-
- private static void AnalyzeMethod(SymbolAnalysisContext context) =>
- AnalyzeSymbol(context, (IMethodSymbol)context.Symbol);
-
- private static void AnalyzeSymbol(SymbolAnalysisContext context, ISymbol symbol) {
- if (context.Compilation.GetTypeByMetadataName(TracedAttributeFullName) is not { } tracedAttributeType) {
- return;
- }
-
- foreach (var attribute in symbol.GetAttributes()) {
- if (!attribute.AttributeClass.IsEqualTo(tracedAttributeType)) {
- continue;
- }
-
- string? activitySourceName = null;
-
- if (attribute.ConstructorArguments is [{ Value: string ctorArg }, ..]) {
- activitySourceName = ctorArg;
- }
-
- foreach (var namedArg in attribute.NamedArguments) {
- if (namedArg is { Key: "ActivitySourceName", Value.Value: string namedValue }) {
- activitySourceName = namedValue;
- break;
- }
- }
-
- if (string.IsNullOrWhiteSpace(activitySourceName)) {
- var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation()
- ?? Location.None;
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, location, symbol.Name));
- }
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0074DeprecatedGenAiAttributeAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0074DeprecatedGenAiAttributeAnalyzer.cs
deleted file mode 100644
index a1081f2..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0074DeprecatedGenAiAttributeAnalyzer.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0074: Detects deprecated GenAI semantic convention attribute names.
-///
-///
-///
-/// The OpenTelemetry GenAI semantic conventions have evolved, with some
-/// attribute names being renamed or deprecated. This analyzer helps
-/// migrate to the current conventions for better interoperability.
-///
-///
-/// Deprecated attributes detected:
-///
-/// - gen_ai.system -> gen_ai.system (still valid, but check usage)
-/// - gen_ai.prompt.tokens -> gen_ai.usage.input_tokens
-/// - gen_ai.completion.tokens -> gen_ai.usage.output_tokens
-/// - prompt_tokens -> gen_ai.usage.input_tokens
-/// - completion_tokens -> gen_ai.usage.output_tokens
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0074DeprecatedGenAiAttributeAnalyzer : AlAnalyzer {
- ///
- /// Mapping of deprecated GenAI attribute names to their replacements.
- ///
-
- /// The diagnostic identifier for AL0074.
- public const string DiagnosticId = "AL0074";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.GenAI,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- internal static bool TryGetDeprecatedAttribute(string attributeName, [NotNullWhen(true)] out string? replacement) =>
- OpenTelemetryDeprecatedSemconvCatalog.TryGetDeprecatedGenAiAttribute(attributeName, out replacement);
-
- /// Registers syntax node actions to analyze string literals for deprecated GenAI attributes.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeStringLiteral, SyntaxKind.StringLiteralExpression);
-
- private static void AnalyzeStringLiteral(SyntaxNodeAnalysisContext context) {
- var literal = (LiteralExpressionSyntax)context.Node;
- var value = literal.Token.ValueText;
-
- if (string.IsNullOrEmpty(value)
- || !TryGetDeprecatedAttribute(value, out var replacement)
- || !IsInTelemetryContext(literal)) {
- return;
- }
-
- var properties = ImmutableDictionary.CreateBuilder();
- properties.Add("Replacement", replacement);
-
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- literal.GetLocation(),
- properties.ToImmutable(),
- value,
- replacement));
- }
-
- private static bool IsInTelemetryContext(SyntaxNode node) {
- var current = node.Parent;
-
- while (current is not null) {
- switch (current) {
- // Dictionary-initializer key (`["foo"] = value`) is lookup-table data, not a tag write.
- // Short-circuit before the InitializerExpressionSyntax case below would spuriously match.
- case ImplicitElementAccessSyntax:
- return false;
- case ElementAccessExpressionSyntax elementAccess
- when IsLikelyTelemetryContainer(GetIdentifierName(elementAccess.Expression)):
- case InvocationExpressionSyntax invocation
- when IsLikelyTelemetryMethod(GetMethodName(invocation)):
- case InitializerExpressionSyntax:
- case AnonymousObjectMemberDeclaratorSyntax:
- return true;
- }
-
- current = current.Parent;
- }
-
- return false;
- }
-
- private static bool IsLikelyTelemetryContainer(string? identifier) {
- if (identifier is null or { Length: 0 }) {
- return false;
- }
-
- var upper = identifier.ToUpperInvariant();
- return upper.ContainsOrdinal("TAG") ||
- upper.ContainsOrdinal("ATTR") ||
- upper.ContainsOrdinal("PROPERTY") ||
- upper.ContainsOrdinal("METADATA") ||
- upper.ContainsOrdinal("SPAN") ||
- upper.ContainsOrdinal("ACTIVITY");
- }
-
- private static bool IsLikelyTelemetryMethod(string? methodName) {
- if (methodName is null or { Length: 0 }) {
- return false;
- }
-
- return methodName switch {
- "SetTag" or "AddTag" or "SetAttribute" or "AddAttribute" => true,
- "SetStatus" or "RecordException" => true,
- _ when methodName.EndsWithOrdinal("Tag") => true,
- _ when methodName.EndsWithOrdinal("Attribute") => true,
- _ when methodName.StartsWithOrdinal("SetTag") => true,
- _ when methodName.StartsWithOrdinal("AddTag") => true,
- _ when methodName.StartsWithOrdinal("Record") => true,
- _ => false
- };
- }
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-
- private static string? GetIdentifierName(ExpressionSyntax expression) =>
- expression switch {
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0075HighCardinalityMetricTagAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0075HighCardinalityMetricTagAnalyzer.cs
deleted file mode 100644
index 4091340..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0075HighCardinalityMetricTagAnalyzer.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0075: Warns about high-cardinality tags on metrics.
-///
-///
-///
-/// High-cardinality tags (like user.id, request.id, session.id) create a
-/// unique time series for each distinct value. This can cause:
-///
-/// - Memory exhaustion in metrics backends (Prometheus, etc.)
-/// - Increased storage costs
-/// - Query performance degradation
-/// - Cardinality explosions that crash collectors
-///
-///
-///
-/// Alternatives to high-cardinality metric tags:
-///
-/// - Use span/trace attributes instead (spans are sampled)
-/// - Aggregate into buckets (e.g., user_type instead of user_id)
-/// - Use exemplars to link metrics to traces
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0075HighCardinalityMetricTagAnalyzer : AlAnalyzer {
- private enum KnownType { TagAttribute, CounterAttribute, HistogramAttribute }
-
- private static readonly string[] s_knownTypeNames = [
- "Qyl.Instrumentation.Instrumentation.TagAttribute",
- "Qyl.Instrumentation.Instrumentation.CounterAttribute",
- "Qyl.Instrumentation.Instrumentation.HistogramAttribute"
- ];
-
- ///
- /// Known high-cardinality tag patterns that should be avoided on metrics.
- ///
- private static readonly string[] s_highCardinalityPatterns = [
- "user.id", "user_id", "userId",
- "request.id", "request_id", "requestId",
- "session.id", "session_id", "sessionId",
- "trace.id", "trace_id", "traceId",
- "span.id", "span_id", "spanId",
- "correlation.id", "correlation_id", "correlationId",
- "transaction.id", "transaction_id", "transactionId",
- "message.id", "message_id", "messageId",
- "order.id", "order_id", "orderId",
- "customer.id", "customer_id", "customerId",
- "account.id", "account_id", "accountId",
- "email", "ip", "ip_address", "user_agent",
- "url", "uri", "path", "query",
- "timestamp", "uuid", "guid"
- ];
-
- /// The diagnostic identifier for AL0075.
- private const string DiagnosticId = "AL0075";
-
- private static readonly DiagnosticDescriptor s_highCardinalityMetricTagRule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Metrics,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_highCardinalityMetricTagRule];
-
- /// Registers compilation start action to resolve metric attribute types once.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var cache = new TypeCache(type => context.Compilation.GetTypeByMetadataName(s_knownTypeNames[(int)type]));
-
- if (cache.Get(KnownType.TagAttribute) is null) {
- return;
- }
-
- if (cache.Get(KnownType.CounterAttribute) is null && cache.Get(KnownType.HistogramAttribute) is null) {
- return;
- }
-
- context.RegisterSyntaxNodeAction(
- ctx => AnalyzeParameterForHighCardinalityTags(ctx, cache),
- SyntaxKind.Parameter);
- }
-
- private static void AnalyzeParameterForHighCardinalityTags(SyntaxNodeAnalysisContext context, TypeCache cache) {
- var parameter = (ParameterSyntax)context.Node;
-
- if (parameter.AttributeLists.Count is 0) {
- return;
- }
-
- if (context.SemanticModel.GetDeclaredSymbol(parameter, context.CancellationToken) is not { ContainingSymbol: IMethodSymbol methodSymbol } parameterSymbol
- || (!cache.HasAttribute(methodSymbol, KnownType.CounterAttribute) && !cache.HasAttribute(methodSymbol, KnownType.HistogramAttribute))
- || cache.GetAttribute(parameterSymbol, KnownType.TagAttribute) is not { ConstructorArguments: [{ Value: string tagName }, ..] }) {
- return;
- }
-
- if (MatchesHighCardinalityPattern(tagName)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_highCardinalityMetricTagRule,
- parameter.GetLocation(),
- tagName));
- }
- }
-
- private static bool MatchesHighCardinalityPattern(string tagName) {
- var normalizedTag = tagName.ToUpperInvariant();
-
- foreach (var pattern in s_highCardinalityPatterns) {
- var normalizedPattern = pattern.ToUpperInvariant();
-
- if (normalizedTag == normalizedPattern) {
- return true;
- }
-
- if (normalizedTag.EndsWithOrdinal("." + normalizedPattern) ||
- normalizedTag.EndsWithOrdinal("_" + normalizedPattern.ReplaceOrdinal(".", "_"))) {
- return true;
- }
-
- if (!normalizedPattern.ContainsOrdinal(".") && !normalizedPattern.ContainsOrdinal("_")
- && (normalizedTag.EndsWithOrdinal("." + normalizedPattern)
- || normalizedTag.EndsWithOrdinal("_" + normalizedPattern))) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0076MissingOTelConfigurationAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0076MissingOTelConfigurationAnalyzer.cs
deleted file mode 100644
index 4dbcb47..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0076MissingOTelConfigurationAnalyzer.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0076: Detects when AddServiceDefaults() or similar setup is called but AddOpenTelemetry() is missing.
-///
-///
-///
-/// This analyzer detects incomplete telemetry configuration where service defaults
-/// are configured but OpenTelemetry is not set up, meaning telemetry will not be exported.
-///
-///
-/// Methods that trigger this diagnostic:
-///
-/// - AddServiceDefaults
-/// - AddQylServiceDefaults
-/// - ConfigureOpenTelemetry
-///
-///
-///
-/// Methods that satisfy the OpenTelemetry requirement:
-///
-/// - AddOpenTelemetry
-/// - WithTracing
-/// - UseOpenTelemetry
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0076MissingOTelConfigurationAnalyzer : AlAnalyzer {
- private static readonly string[] s_serviceDefaultsMethods = [
- "AddServiceDefaults",
- "AddQylServiceDefaults",
- "ConfigureOpenTelemetry"
- ];
-
- private static readonly string[] s_oTelConfigurationMethods = [
- "AddOpenTelemetry",
- "WithTracing",
- "UseOpenTelemetry"
- ];
-
- /// The diagnostic identifier for AL0076.
- private const string DiagnosticId = "AL0076";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax tree actions to analyze OpenTelemetry configuration.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
-
- private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess
- || !s_serviceDefaultsMethods.Contains(memberAccess.Name.Identifier.Text)) {
- return;
- }
-
- if (invocation.Ancestors().OfType().FirstOrDefault() is not { } containingMethod) {
- return;
- }
-
- var allInvocations = new HashSet();
- foreach (var inv in containingMethod.DescendantNodes().OfType()) {
- if (GetMethodName(inv) is { } name) {
- allInvocations.Add(name);
- }
- }
-
- if (!s_oTelConfigurationMethods.Any(allInvocations.Contains)) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, invocation.GetLocation()));
- }
- }
-
- // Only match member access to avoid matching local methods with same name
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0077DuplicateInstrumentationAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0077DuplicateInstrumentationAnalyzer.cs
deleted file mode 100644
index 09ff05c..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0077DuplicateInstrumentationAnalyzer.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using MsOperationExtensions = Microsoft.CodeAnalysis.Operations.OperationExtensions;
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0077: Detects duplicate instrumentation - methods with both auto-instrumentation and manual spans.
-///
-///
-///
-/// When a method is instrumented by both auto-instrumentation (e.g., [Traced] attribute or
-/// interceptor) AND manual Activity.StartActivity spans, it creates duplicate nested spans that:
-///
-/// - Increase telemetry volume unnecessarily
-/// - Cause confusion in trace visualization
-/// - May lead to incorrect span duration calculations
-///
-///
-///
-/// Example of problematic code:
-///
-/// [Traced("MyApp")]
-/// public void ProcessOrder()
-/// {
-/// using var activity = ActivitySource.StartActivity("ProcessOrder"); // Duplicate!
-/// // ...
-/// }
-///
-///
-///
-/// Solution: Choose one instrumentation approach per method.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0077DuplicateInstrumentationAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0077.
- private const string DiagnosticId = "AL0077";
-
- private const string TracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers actions to analyze methods with [Traced] attribute.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- if (compilationContext.Compilation.GetTypeByMetadataName(TracedAttributeFullName) is not { } tracedAttributeType) {
- return;
- }
-
- compilationContext.RegisterOperationBlockAction(ctx =>
- AnalyzeOperationBlock(ctx, tracedAttributeType));
- });
- }
-
- private static void AnalyzeOperationBlock(OperationBlockAnalysisContext context, INamedTypeSymbol tracedAttributeType) {
- if (context.OwningSymbol is not IMethodSymbol method) {
- return;
- }
-
- // [Traced] on the method itself, or inherited from the containing type
- if (!HasTracedAttribute(method, tracedAttributeType)
- && (method.ContainingType is null || !HasTracedAttribute(method.ContainingType, tracedAttributeType))) {
- return;
- }
-
- foreach (var operationBlock in context.OperationBlocks) {
- if (ContainsStartActivityCall(operationBlock)) {
- var location = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(context.CancellationToken).GetLocation()
- ?? Location.None;
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, location, method.Name));
- return;
- }
- }
- }
-
- private static bool HasTracedAttribute(ISymbol symbol, INamedTypeSymbol tracedAttributeType) {
- foreach (var attribute in symbol.GetAttributes()) {
- if (attribute.AttributeClass.IsEqualTo(tracedAttributeType)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool ContainsStartActivityCall(IOperation operation) {
- foreach (var descendant in MsOperationExtensions.DescendantsAndSelf(operation)) {
- if (descendant is IInvocationOperation invocation &&
- IsStartActivityMethod(invocation.TargetMethod)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool IsStartActivityMethod(IMethodSymbol method) =>
- method is { Name: "StartActivity", ContainingType.Name: "ActivitySource" };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0078InvalidActivitySourceNameAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0078InvalidActivitySourceNameAnalyzer.cs
deleted file mode 100644
index 413a874..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0078InvalidActivitySourceNameAnalyzer.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0078: Detects ActivitySource names that don't follow reverse-DNS naming convention.
-///
-///
-///
-/// ActivitySource names should follow the reverse-DNS convention (e.g., 'company.product.component').
-/// Valid names must:
-///
-/// - Contain at least one dot to indicate hierarchical namespace
-/// - Use only lowercase letters, digits, dots, and hyphens
-/// - Not contain spaces or other invalid characters
-/// - Not be empty or whitespace-only
-///
-///
-///
-/// This naming convention ensures consistent identification across telemetry backends
-/// and follows OpenTelemetry best practices.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0078InvalidActivitySourceNameAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0078.
- private const string DiagnosticId = "AL0078";
-
- private const string ActivitySourceTypeName = "System.Diagnostics.ActivitySource";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.RequiredFix);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze ActivitySource creation.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
-
- private static void AnalyzeObjectCreation(OperationAnalysisContext context) {
- var objectCreation = (IObjectCreationOperation)context.Operation;
-
- if (objectCreation.Type?.ToDisplayString() != ActivitySourceTypeName
- || objectCreation.Arguments is not [{ Value.ConstantValue: { HasValue: true, Value: string sourceName } } firstArg, ..]) {
- return;
- }
-
- if (!IsValidActivitySourceName(sourceName)) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, firstArg.Syntax.GetLocation(), sourceName));
- }
- }
-
- private static bool IsValidActivitySourceName(string name) {
- if (string.IsNullOrWhiteSpace(name) || !name.ContainsOrdinal(".") || name.ContainsOrdinal(" ")) {
- return false;
- }
-
- var segments = name.Split('.');
- foreach (var segment in segments) {
- if (string.IsNullOrEmpty(segment)) {
- return false;
- }
-
- // Allow letters (upper and lower), digits, and hyphens
- foreach (var c in segment) {
- if (!char.IsLetter(c) && !char.IsDigit(c) && c != '-') {
- return false;
- }
- }
- }
-
- return true;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0079ManualSpanRecommendedAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0079ManualSpanRecommendedAnalyzer.cs
deleted file mode 100644
index cdeb3b5..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0079ManualSpanRecommendedAnalyzer.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0079: Detects complex async patterns in [Traced] methods where manual instrumentation
-/// may provide better observability than auto-instrumentation.
-///
-///
-///
-/// Auto-instrumentation from [Traced] creates a single span for the method execution.
-/// However, complex async patterns may not be fully captured:
-///
-/// - Task.WhenAll: Parallel tasks run concurrently but appear as sequential in traces
-/// - Parallel.ForEach: Parallel iterations are not individually traced
-/// - Multiple awaits: Gaps between awaits may obscure actual execution flow
-/// - ConfigureAwait(false): Context loss may affect trace propagation
-///
-///
-///
-/// For these patterns, consider using manual Activity.StartActivity() to create
-/// explicit child spans that capture the parallel execution branches.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0079ManualSpanRecommendedAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0079.
- private const string DiagnosticId = "AL0079";
-
- private const string TracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.HiddenByDefault);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax node actions to analyze methods with [Traced] attribute.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- if (compilationContext.Compilation.GetTypeByMetadataName(TracedAttributeFullName) is not { } tracedAttributeType) {
- return;
- }
-
- compilationContext.RegisterSyntaxNodeAction(
- ctx => AnalyzeMethod(ctx, tracedAttributeType),
- SyntaxKind.MethodDeclaration);
- });
- }
-
- private static void AnalyzeMethod(SyntaxNodeAnalysisContext context, INamedTypeSymbol tracedAttributeType) {
- var method = (MethodDeclarationSyntax)context.Node;
-
- if (!method.Modifiers.Any(SyntaxKind.AsyncKeyword)) {
- return;
- }
-
- if (ModelExtensions.GetDeclaredSymbol(context.SemanticModel, method, context.CancellationToken) is not { } methodSymbol
- || (!HasTracedAttribute(methodSymbol, tracedAttributeType)
- && !HasTracedAttribute(methodSymbol.ContainingType, tracedAttributeType))) {
- return;
- }
-
- var complexPatterns = DetectComplexPatterns(method, context.SemanticModel, context.CancellationToken);
-
- if (complexPatterns.Length > 0) {
- context.ReportDiagnostic(s_rule, method.Identifier.GetLocation(), methodSymbol.Name, string.Join(", ", complexPatterns));
- }
- }
-
- private static bool HasTracedAttribute(ISymbol symbol, INamedTypeSymbol tracedAttributeType) {
- foreach (var attribute in symbol.GetAttributes()) {
- if (attribute.AttributeClass.IsEqualTo(tracedAttributeType)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static ImmutableArray DetectComplexPatterns(
- MethodDeclarationSyntax method,
- SemanticModel semanticModel,
- CancellationToken cancellationToken) {
- if (method.Body is null && method.ExpressionBody is null) {
- return [];
- }
-
- var walker = new ComplexPatternWalker(semanticModel, cancellationToken);
- walker.Visit(method);
-
- var patterns = ImmutableArray.CreateBuilder();
-
- if (walker.HasTaskWhenAll) {
- patterns.Add("Task.WhenAll");
- }
-
- if (walker.HasTaskWhenAny) {
- patterns.Add("Task.WhenAny");
- }
-
- if (walker.HasParallelForEach) {
- patterns.Add("Parallel.ForEach/ForEachAsync");
- }
-
- if (walker.HasParallelFor) {
- patterns.Add("Parallel.For/ForAsync");
- }
-
- if (walker.HasConfigureAwaitFalse) {
- patterns.Add("ConfigureAwait(false)");
- }
-
- if (walker.AwaitCount >= 3) {
- patterns.Add($"multiple awaits ({walker.AwaitCount})");
- }
-
- return patterns.ToImmutable();
- }
-
- ///
- /// Syntax walker that detects complex async patterns in method bodies.
- ///
- private sealed partial class ComplexPatternWalker(SemanticModel semanticModel, CancellationToken cancellationToken)
- : CSharpSyntaxWalker {
- public int AwaitCount { get; private set; }
- public bool HasTaskWhenAll { get; private set; }
- public bool HasTaskWhenAny { get; private set; }
- public bool HasParallelForEach { get; private set; }
- public bool HasParallelFor { get; private set; }
- public bool HasConfigureAwaitFalse { get; private set; }
-
- public override void VisitAwaitExpression(AwaitExpressionSyntax node) {
- AwaitCount++;
- base.VisitAwaitExpression(node);
- }
-
- public override void VisitInvocationExpression(InvocationExpressionSyntax node) {
- if (ModelExtensions.GetSymbolInfo(semanticModel, node, cancellationToken).Symbol is IMethodSymbol methodSymbol) {
- switch (methodSymbol.ContainingType?.ToDisplayString(), methodSymbol.Name) {
- case ("System.Threading.Tasks.Task", "WhenAll"):
- HasTaskWhenAll = true;
- break;
- case ("System.Threading.Tasks.Task", "WhenAny"):
- HasTaskWhenAny = true;
- break;
- case ("System.Threading.Tasks.Parallel", "ForEach" or "ForEachAsync"):
- HasParallelForEach = true;
- break;
- case ("System.Threading.Tasks.Parallel", "For" or "ForAsync"):
- HasParallelFor = true;
- break;
- case (_, "ConfigureAwait") when methodSymbol.Parameters.Length == 1
- && node.ArgumentList.Arguments is [{ Expression: LiteralExpressionSyntax { Token.Value: false } }]:
- HasConfigureAwaitFalse = true;
- break;
- }
- }
-
- base.VisitInvocationExpression(node);
- }
-
- // Nested lambdas/local functions have their own scope -- don't count their awaits
- public override void VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node) { }
-
- public override void VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) { }
-
- public override void VisitLocalFunctionStatement(LocalFunctionStatementSyntax node) { }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0083InsecureEndpointAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0083InsecureEndpointAnalyzer.cs
deleted file mode 100644
index 7a103ae..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0083InsecureEndpointAnalyzer.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0083: Detects HTTP endpoints used where HTTPS is expected.
-///
-///
-///
-/// HTTP endpoints expose sensitive data in transit to interception and
-/// man-in-the-middle attacks. This analyzer flags:
-///
-/// - String literals starting with "http://" (not "https://")
-/// - Used in HttpClient, OTLP exporter, API client configurations
-///
-///
-///
-/// The following are excluded (development use):
-///
-/// - localhost (e.g., http://localhost:5000)
-/// - 127.0.0.1 (e.g., http://127.0.0.1:5000)
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0083InsecureEndpointAnalyzer : AlAnalyzer {
- private const string HttpPrefix = "http://";
- private const string HttpsPrefix = "https://";
-
- private static readonly string[] s_localhostPatterns = [
- "localhost",
- "127.0.0.1",
- "[::1]"
- ];
-
- private static readonly string[] s_endpointPropertyNames = [
- "Endpoint",
- "BaseAddress",
- "Url",
- "Uri",
- "Address",
- "Host",
- "CollectorEndpoint",
- "OtlpEndpoint",
- "ExporterEndpoint",
- "ServiceUrl",
- "ApiUrl",
- "ServerUrl"
- ];
-
- /// The diagnostic identifier for AL0083.
- private const string DiagnosticId = "AL0083";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Configuration,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze endpoint assignments and constructor arguments.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment);
- context.RegisterOperationAction(AnalyzeArgument, OperationKind.Argument);
- context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
- }
-
- private static void AnalyzeAssignment(OperationAnalysisContext context) {
- var assignment = (ISimpleAssignmentOperation)context.Operation;
-
- if (GetPropertyName(assignment.Target) is not { } propertyName || !IsEndpointProperty(propertyName)) {
- return;
- }
-
- CheckForInsecureEndpoint(context, assignment.Value);
- }
-
- private static void AnalyzeArgument(OperationAnalysisContext context) {
- var argument = (IArgumentOperation)context.Operation;
-
- // AnalyzeObjectCreation handles Uri/HttpClient creation arguments
- if (argument.Parent is IObjectCreationOperation { Type.Name: "Uri" or "HttpClient" }) {
- return;
- }
-
- if (argument.Parameter?.Name is not { } parameterName || !IsEndpointProperty(parameterName)) {
- return;
- }
-
- CheckForInsecureEndpoint(context, argument.Value);
- }
-
- private static void AnalyzeObjectCreation(OperationAnalysisContext context) {
- var creation = (IObjectCreationOperation)context.Operation;
-
- if (creation.Type?.Name is not ("Uri" or "HttpClient")) {
- return;
- }
-
- foreach (var argument in creation.Arguments) {
- CheckForInsecureEndpoint(context, argument.Value);
- }
- }
-
- private static void CheckForInsecureEndpoint(OperationAnalysisContext context, IOperation operation) {
- var value = operation.UnwrapAllConversions();
-
- if (value.ConstantValue is { HasValue: true, Value: string endpoint } && IsInsecureEndpoint(endpoint)) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, operation.Syntax.GetLocation(), endpoint));
- }
- }
-
- private static string? GetPropertyName(IOperation target) =>
- target switch {
- IPropertyReferenceOperation propRef => propRef.Property.Name,
- IMemberReferenceOperation memberRef => memberRef.Member.Name,
- _ => null
- };
-
- private static bool IsEndpointProperty(string name) {
- foreach (var pattern in s_endpointPropertyNames) {
- if (name.ContainsIgnoreCase(pattern)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool IsInsecureEndpoint(string endpoint) {
- if (!endpoint.StartsWithOrdinal(HttpPrefix) || endpoint.StartsWithOrdinal(HttpsPrefix)) {
- return false;
- }
-
- // Exclude localhost patterns (development use), but not "localhost-prod" style prefixes
- var hostPart = endpoint.Substring(HttpPrefix.Length);
- foreach (var localhost in s_localhostPatterns) {
- if (hostPart.StartsWithIgnoreCase(localhost)
- && (hostPart.Length == localhost.Length || hostPart[localhost.Length] is ':' or '/' or '?')) {
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0085InvalidAttributeValueAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0085InvalidAttributeValueAnalyzer.cs
deleted file mode 100644
index 1cecbdc..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0085InvalidAttributeValueAnalyzer.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0085: Detects attribute values that violate OTel semantic convention specifications.
-///
-///
-///
-/// Validates that known semantic convention attributes have correct value formats:
-///
-/// - http.response.status_code - must be an integer (100-599)
-/// - gen_ai.system - must be a known provider (openai, anthropic, etc.)
-/// - error.type - should be an exception type or error code
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0085InvalidAttributeValueAnalyzer : AlAnalyzer {
- /// Attribute validators by attribute name.
- private static readonly Dictionary s_validators = new(StringComparer.OrdinalIgnoreCase) {
- ["http.response.status_code"] = new AttributeValidator(
- ValidateHttpStatusCode,
- "integer between 100-599"),
- ["http.request.method"] = new AttributeValidator(
- ValidateHttpMethod,
- "valid HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE, CONNECT, _OTHER)"),
- ["gen_ai.provider.name"] = new AttributeValidator(
- ValidateGenAiProvider,
- "one of the official provider names such as openai, anthropic, azure.ai.inference, azure.ai.openai, gcp.vertex_ai, gcp.gemini, cohere, aws.bedrock"),
- ["gen_ai.operation.name"] = new AttributeValidator(
- ValidateGenAiOperation,
- "one of: chat, generate_content, text_completion, embeddings, retrieval, create_agent, invoke_agent, execute_tool, invoke_workflow"),
- ["gen_ai.request.max_tokens"] = new AttributeValidator(
- ValidatePositiveInteger,
- "positive integer"),
- ["gen_ai.request.temperature"] = new AttributeValidator(
- ValidateTemperature,
- "number between 0.0 and 2.0"),
- ["gen_ai.request.top_p"] = new AttributeValidator(
- ValidateProbability,
- "number between 0.0 and 1.0"),
- ["gen_ai.response.finish_reasons"] = new AttributeValidator(
- ValidateFinishReason,
- "one of: stop, length, content_filter, tool_calls, error"),
- ["gen_ai.usage.input_tokens"] = new AttributeValidator(
- ValidateNonNegativeInteger,
- "non-negative integer"),
- ["gen_ai.usage.output_tokens"] = new AttributeValidator(
- ValidateNonNegativeInteger,
- "non-negative integer"),
- ["rpc.response.status_code"] = new AttributeValidator(
- ValidateRpcResponseStatusCode,
- "non-empty status code string such as OK, DEADLINE_EXCEEDED, or -32602"),
- ["url.scheme"] = new AttributeValidator(
- ValidateUrlScheme,
- "one of: http, https, ftp, ws, wss")
- };
-
- /// The diagnostic identifier for AL0085.
- private const string DiagnosticId = "AL0085";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.RequiredFix);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze SetTag and SetAttribute calls.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
-
- private static void AnalyzeInvocation(OperationAnalysisContext context) {
- var invocation = (IInvocationOperation)context.Operation;
-
- if (invocation.TargetMethod.Name is not ("SetTag" or "SetAttribute" or "Add")
- || invocation.Arguments.Length < 2
- || invocation.Arguments[0].Value.UnwrapAllConversions().ConstantValue is not { HasValue: true, Value: string attributeName }
- || !s_validators.TryGetValue(attributeName, out var validator)) {
- return;
- }
-
- // Unwrap implicit conversions (e.g. string -> object? when the Activity overload takes object).
- // Without this, callers that pass a string literal to a `object?` parameter lose ConstantValue
- // and the validator never runs.
- var valueArg = invocation.Arguments[1].Value.UnwrapAllConversions();
-
- if (valueArg.ConstantValue.HasValue) {
- var valueString = valueArg.ConstantValue.Value?.ToString() ?? string.Empty;
-
- if (!validator.Validate(valueString)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- invocation.Arguments[1].Syntax.GetLocation(),
- attributeName,
- valueString,
- validator.ExpectedFormat));
- }
- }
- }
-
- private static bool ValidateHttpStatusCode(string value) =>
- int.TryParse(value, out var code) && code is >= 100 and <= 599;
-
- private static bool ValidateHttpMethod(string value) =>
- value is "GET" or "POST" or "PUT" or "DELETE" or "PATCH"
- or "HEAD" or "OPTIONS" or "TRACE" or "CONNECT" or "_OTHER";
-
- private static bool ValidateGenAiProvider(string value) =>
- OpenTelemetryGenAiSemconvFacts.IsValidProviderName(value);
-
- private static bool ValidateGenAiOperation(string value) =>
- OpenTelemetryGenAiSemconvFacts.IsValidOperationName(value);
-
- private static bool ValidatePositiveInteger(string value) =>
- int.TryParse(value, out var num) && num > 0;
-
- private static bool ValidateNonNegativeInteger(string value) =>
- int.TryParse(value, out var num) && num >= 0;
-
- private static bool ValidateTemperature(string value) =>
- double.TryParse(value, out var temp) && temp is >= 0.0 and <= 2.0;
-
- private static bool ValidateProbability(string value) =>
- double.TryParse(value, out var prob) && prob is >= 0.0 and <= 1.0;
-
- private static bool ValidateFinishReason(string value) =>
- value.EqualsIgnoreCase("stop")
- || value.EqualsIgnoreCase("length")
- || value.EqualsIgnoreCase("content_filter")
- || value.EqualsIgnoreCase("tool_calls")
- || value.EqualsIgnoreCase("error");
-
- private static bool ValidateRpcResponseStatusCode(string value) =>
- !string.IsNullOrWhiteSpace(value);
-
- private static bool ValidateUrlScheme(string value) =>
- value.EqualsIgnoreCase("http")
- || value.EqualsIgnoreCase("https")
- || value.EqualsIgnoreCase("ftp")
- || value.EqualsIgnoreCase("ws")
- || value.EqualsIgnoreCase("wss");
-
- /// Encapsulates validation logic and expected format message.
- private sealed partial class AttributeValidator(Func validate, string expectedFormat) {
- public string ExpectedFormat { get; } = expectedFormat;
-
- public bool Validate(string value) => validate(value);
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0086IncorrectAttributeTypeAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0086IncorrectAttributeTypeAnalyzer.cs
deleted file mode 100644
index c1f2dae..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0086IncorrectAttributeTypeAnalyzer.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0086: Detects OpenTelemetry attributes set with incorrect types.
-///
-///
-///
-/// OpenTelemetry semantic conventions specify expected types for attributes.
-/// Using incorrect types (e.g., passing a string where an integer is expected)
-/// can cause issues with telemetry backends, dashboards, and aggregation logic.
-///
-///
-/// Examples of common type mismatches:
-///
-/// - gen_ai.usage.input_tokens: should be int, not string
-/// - http.response.status_code: should be int, not string
-/// - db.operation.batch.size: should be int, not string
-/// - rpc.response.status_code: should be string, not int
-///
-///
-///
-/// The analyzer identifies SetTag/SetAttribute calls where the attribute name
-/// matches a known semantic convention and the value type does not match
-/// the expected type per the specification.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0086IncorrectAttributeTypeAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0086.
- private const string DiagnosticId = "AL0086";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverity.Warning);
-
- ///
- /// Maps attribute names to their expected types per OTel semantic conventions.
- ///
- private static readonly Dictionary s_attributeTypeMap = new(StringComparer.Ordinal) {
- // GenAI token counts (must be integers)
- ["gen_ai.usage.input_tokens"] = ExpectedType.WholeNumber,
- ["gen_ai.usage.output_tokens"] = ExpectedType.WholeNumber,
- ["gen_ai.request.max_tokens"] = ExpectedType.WholeNumber,
- ["gen_ai.response.finish_reasons"] = ExpectedType.CharacterSequenceArray,
-
- // GenAI numeric attributes
- ["gen_ai.request.temperature"] = ExpectedType.FloatingPoint,
- ["gen_ai.request.top_p"] = ExpectedType.FloatingPoint,
- ["gen_ai.request.top_k"] = ExpectedType.WholeNumber,
- ["gen_ai.request.frequency_penalty"] = ExpectedType.FloatingPoint,
- ["gen_ai.request.presence_penalty"] = ExpectedType.FloatingPoint,
-
- // HTTP attributes (must be integers)
- ["http.response.status_code"] = ExpectedType.WholeNumber,
- ["http.request.body.size"] = ExpectedType.WholeNumber,
- ["http.response.body.size"] = ExpectedType.WholeNumber,
- ["http.request.resend_count"] = ExpectedType.WholeNumber,
-
- // Database attributes
- ["db.operation.batch.size"] = ExpectedType.WholeNumber,
- ["db.response.status_code"] = ExpectedType.CharacterSequence,
-
- // RPC attributes (semconv 1.40 unified gRPC and ConnectRPC into a single string status_code)
- ["rpc.response.status_code"] = ExpectedType.CharacterSequence,
-
- // Network attributes
- ["network.peer.port"] = ExpectedType.WholeNumber,
- ["server.port"] = ExpectedType.WholeNumber,
- ["client.port"] = ExpectedType.WholeNumber,
- ["url.port"] = ExpectedType.WholeNumber,
-
- // Thread/process attributes
- ["thread.id"] = ExpectedType.WholeNumber,
- ["process.pid"] = ExpectedType.WholeNumber,
- ["process.parent_pid"] = ExpectedType.WholeNumber
- };
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze invocation expressions.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
-
- private static void AnalyzeInvocation(OperationAnalysisContext context) {
- var invocation = (IInvocationOperation)context.Operation;
-
- if (!IsAttributeSetterMethod(invocation.TargetMethod)
- || invocation.Arguments.Length < 2
- || !invocation.Arguments[0].Value.TryGetConstantValue(out string? attributeName)
- || attributeName is null
- || !s_attributeTypeMap.TryGetValue(attributeName, out var expectedType)) {
- return;
- }
-
- var valueArg = invocation.Arguments[1];
- var unwrapped = valueArg.Value.UnwrapAllConversions();
-
- if (unwrapped.Type is not { } valueType || IsTypeMatch(valueType, expectedType)) {
- return;
- }
-
- context.ReportDiagnostic(s_rule, valueArg.Syntax.GetLocation(), attributeName,
- GetTypeName(expectedType), valueType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
- }
-
- private static bool IsAttributeSetterMethod(IMethodSymbol method) =>
- method.Name switch {
- "SetTag" or "SetAttribute" or "AddTag" or "SetCustomProperty" => true,
- "Add" => method.ContainingType?.Name is { } name
- && (name.ContainsOrdinal("Tag") || name.ContainsOrdinal("Attribute")),
- _ => false
- };
-
- private static bool IsTypeMatch(ITypeSymbol actualType, ExpectedType expectedType) {
- return expectedType switch {
- ExpectedType.WholeNumber => IsIntegerType(actualType),
- ExpectedType.WholeNumber64 => IsLongType(actualType),
- ExpectedType.FloatingPoint => IsNumericType(actualType),
- ExpectedType.CharacterSequence => actualType.SpecialType == SpecialType.System_String,
- ExpectedType.TrueOrFalse => actualType.SpecialType == SpecialType.System_Boolean,
- ExpectedType.CharacterSequenceArray => IsStringArrayType(actualType),
- _ => true // Unknown expected type, don't flag
- };
- }
-
- private static bool IsIntegerType(ITypeSymbol type) =>
- type.SpecialType is
- SpecialType.System_Int32 or
- SpecialType.System_Int64 or
- SpecialType.System_Int16 or
- SpecialType.System_Byte or
- SpecialType.System_UInt32 or
- SpecialType.System_UInt64 or
- SpecialType.System_UInt16 or
- SpecialType.System_SByte;
-
- private static bool IsLongType(ITypeSymbol type) =>
- type.SpecialType is SpecialType.System_Int64 or SpecialType.System_UInt64 ||
- IsIntegerType(type); // Allow int promotion to long
-
- private static bool IsNumericType(ITypeSymbol type) =>
- IsIntegerType(type) ||
- type.SpecialType is
- SpecialType.System_Single or
- SpecialType.System_Double or
- SpecialType.System_Decimal;
-
- private static bool IsStringArrayType(ITypeSymbol type) =>
- type switch {
- IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_String } => true,
- INamedTypeSymbol { IsGenericType: true, TypeArguments: [{ SpecialType: SpecialType.System_String }] } namedType =>
- namedType.Name.ContainsOrdinal("List")
- || namedType.Name.ContainsOrdinal("Enumerable")
- || namedType.Name.ContainsOrdinal("Collection")
- || namedType.Name.ContainsOrdinal("Array"),
- _ => false
- };
-
- private static string GetTypeName(ExpectedType expectedType) =>
- expectedType switch {
- ExpectedType.WholeNumber => "int",
- ExpectedType.WholeNumber64 => "long",
- ExpectedType.FloatingPoint => "double",
- ExpectedType.CharacterSequence => "string",
- ExpectedType.TrueOrFalse => "bool",
- ExpectedType.CharacterSequenceArray => "string[]",
- _ => "unknown"
- };
-
- private enum ExpectedType {
- WholeNumber,
- WholeNumber64,
- FloatingPoint,
- CharacterSequence,
- TrueOrFalse,
- CharacterSequenceArray
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0088SensitiveDataInAttributeAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0088SensitiveDataInAttributeAnalyzer.cs
deleted file mode 100644
index d4efffb..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0088SensitiveDataInAttributeAnalyzer.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0088: Detects potential PII or credential data in span attributes.
-///
-///
-///
-/// Span attributes containing sensitive data (passwords, secrets, tokens, API keys,
-/// SSNs, credit card numbers) can leak sensitive information to telemetry backends
-/// where it may be stored, logged, or exposed to unauthorized users.
-///
-///
-/// The analyzer detects sensitive patterns in two ways:
-///
-/// - Attribute names containing sensitive keywords (password, secret, token, etc.)
-/// - Values coming from variables with sensitive names
-///
-///
-///
-/// Context detection uses heuristics: the analyzer looks for patterns like
-/// SetTag, AddTag, dictionary indexers on telemetry containers, and invocations
-/// of methods containing "Attribute" or "Tag".
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0088SensitiveDataInAttributeAnalyzer : AlAnalyzer {
- ///
- /// Patterns in attribute names that indicate sensitive data.
- ///
- private static readonly string[] s_sensitiveAttributeNamePatterns = [
- // Credentials
- "password",
- "passwd",
- "pwd",
- "secret",
- "credential",
- "credentials",
- "auth",
- "authorization",
- "bearer",
-
- // Tokens and keys
- "token",
- "api_key",
- "apikey",
- "api.key",
- "private_key",
- "privatekey",
- "private.key",
- "access_key",
- "accesskey",
- "access.key",
- "secret_key",
- "secretkey",
- "secret.key",
- "encryption_key",
- "encryptionkey",
-
- // PII
- "ssn",
- "social_security",
- "socialsecurity",
- "social.security",
- "credit_card",
- "creditcard",
- "credit.card",
- "card_number",
- "cardnumber",
- "card.number",
- "cvv",
- "pin",
-
- // Connection strings
- "connection_string",
- "connectionstring",
- "connection.string",
- "conn_str"
- ];
-
- ///
- /// Known telemetry method patterns.
- ///
- private static readonly HashSet s_telemetryMethodPatterns =
- new(StringComparer.OrdinalIgnoreCase) {
- "SetTag",
- "AddTag",
- "SetAttribute",
- "AddAttribute",
- "SetCustomProperty"
- };
-
- /// The diagnostic identifier for AL0088.
- private const string DiagnosticId = "AL0088";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverity.Warning);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax node actions to analyze string literals for sensitive attribute names.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeStringLiteral, SyntaxKind.StringLiteralExpression);
-
- private static void AnalyzeStringLiteral(SyntaxNodeAnalysisContext context) {
- var literal = (LiteralExpressionSyntax)context.Node;
- var value = literal.Token.ValueText;
-
- if (string.IsNullOrEmpty(value)
- || !IsLikelyAttributeName(literal)
- || !IsInTelemetryContext(literal)
- || !ContainsSensitivePattern(value)) {
- return;
- }
-
- context.ReportDiagnostic(s_rule, literal.GetLocation(), value);
- }
-
- private static bool IsLikelyAttributeName(LiteralExpressionSyntax literal) =>
- literal.Parent switch {
- // First argument in a method call (key position)
- ArgumentSyntax { Parent: ArgumentListSyntax argumentList } argument
- when argumentList.Arguments.FirstOrDefault() == argument => true,
- // Dictionary/indexer access
- ArgumentSyntax { Parent: BracketedArgumentListSyntax } => true,
- // Key in object initializer
- AssignmentExpressionSyntax { Parent: InitializerExpressionSyntax } => true,
- _ => false
- };
-
- private static bool IsInTelemetryContext(SyntaxNode node) {
- var current = node.Parent;
-
- while (current is not null) {
- if (IsTelemetryElementAccess(current) ||
- IsTelemetryInvocation(current) ||
- IsTelemetryInitializer(current)) {
- return true;
- }
-
- current = current.Parent;
- }
-
- return false;
- }
-
- private static bool IsTelemetryElementAccess(SyntaxNode node) =>
- node is ElementAccessExpressionSyntax elementAccess &&
- GetIdentifierName(elementAccess.Expression) is { } identifier &&
- IsLikelyTelemetryContainer(identifier);
-
- private static bool IsTelemetryInvocation(SyntaxNode node) =>
- node is InvocationExpressionSyntax invocation
- && GetMethodName(invocation) is { } methodName
- && (s_telemetryMethodPatterns.Contains(methodName)
- || methodName.ContainsIgnoreCase("ATTRIBUTE")
- || methodName.ContainsIgnoreCase("TAG"));
-
- private static bool IsTelemetryInitializer(SyntaxNode node) =>
- node is InitializerExpressionSyntax { Parent: ObjectCreationExpressionSyntax creation } &&
- IsTelemetryTypeName(creation.Type.ToString());
-
- private static bool IsTelemetryTypeName(string typeName) =>
- typeName.ContainsOrdinal("Tag") ||
- typeName.ContainsOrdinal("Attribute") ||
- typeName.ContainsOrdinal("KeyValuePair");
-
- private static bool IsLikelyTelemetryContainer(string identifier) =>
- identifier.ContainsIgnoreCase("ATTRIBUTE") ||
- identifier.ContainsIgnoreCase("TAG") ||
- identifier.ContainsIgnoreCase("ATTR") ||
- identifier.EqualsIgnoreCase("ATTRS") ||
- identifier.ContainsIgnoreCase("SPAN") ||
- identifier.ContainsIgnoreCase("ACTIVITY");
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-
- private static string? GetIdentifierName(ExpressionSyntax expression) =>
- expression switch {
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- _ => null
- };
-
- private static bool ContainsSensitivePattern(string attributeName) {
- var normalizedName = attributeName.ToUpperInvariant();
-
- foreach (var pattern in s_sensitiveAttributeNamePatterns) {
- var normalizedPattern = pattern.ToUpperInvariant();
-
- if (normalizedName == normalizedPattern || normalizedName.ContainsOrdinal(normalizedPattern)) {
- return true;
- }
-
- // Handle separator variations (dot vs underscore)
- if (normalizedName.ContainsOrdinal(normalizedPattern.ReplaceOrdinal(".", "_"))
- || normalizedName.ContainsOrdinal(normalizedPattern.ReplaceOrdinal("_", "."))) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0089MissingOtlpConfigurationAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0089MissingOtlpConfigurationAnalyzer.cs
deleted file mode 100644
index 6561259..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0089MissingOtlpConfigurationAnalyzer.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0089: Detects OTLP exporter calls without explicit endpoint configuration.
-///
-///
-///
-/// When using UseOtlpExporter() or AddOtlpExporter() without an explicit endpoint,
-/// telemetry will be sent to the default localhost endpoint or fail to export
-/// if no collector is running locally.
-///
-///
-/// Methods that trigger this diagnostic (when called without endpoint configuration):
-///
-/// - UseOtlpExporter
-/// - AddOtlpExporter
-///
-///
-///
-/// Ways to satisfy the endpoint configuration requirement:
-///
-/// - Pass options delegate with Endpoint property set
-/// - Pass explicit endpoint URI parameter
-/// - Call after Environment.SetEnvironmentVariable for OTEL_EXPORTER_OTLP_ENDPOINT
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0089MissingOtlpConfigurationAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0089.
- private const string DiagnosticId = "AL0089";
-
- private static readonly string[] s_otlpExporterMethods = [
- "UseOtlpExporter",
- "AddOtlpExporter"
- ];
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax tree actions to analyze OTLP exporter configuration.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
-
- private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess
- || !s_otlpExporterMethods.Contains(memberAccess.Name.Identifier.Text)
- || HasExplicitEndpointConfiguration(invocation)
- || HasEnvironmentVariableSet(invocation)) {
- return;
- }
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, invocation.GetLocation()));
- }
-
- private static bool HasExplicitEndpointConfiguration(InvocationExpressionSyntax invocation) {
- var arguments = invocation.ArgumentList.Arguments;
-
- // UseOtlpExporter(OtlpExportProtocol.Grpc, new Uri("...")) pattern
- if (arguments is [_, { Expression: ObjectCreationExpressionSyntax objCreation }, ..]
- && objCreation.Type.ToString().ContainsOrdinal("Uri")) {
- return true;
- }
-
- foreach (var arg in arguments) {
- if (arg.Expression is SimpleLambdaExpressionSyntax or ParenthesizedLambdaExpressionSyntax or
- AnonymousMethodExpressionSyntax
- && LambdaSetsEndpoint(arg.Expression)) {
- return true;
- }
-
- if (arg.NameColon?.Name.Identifier.Text.EqualsIgnoreCase("endpoint") == true) {
- return true;
- }
-
- if (arg.Expression is ObjectCreationExpressionSyntax creation
- && creation.Initializer?.Expressions.Any(static e =>
- e is AssignmentExpressionSyntax assignment
- && assignment.Left.ToString().EqualsIgnoreCase("Endpoint")) == true) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool LambdaSetsEndpoint(ExpressionSyntax lambda) {
- if (lambda switch {
- SimpleLambdaExpressionSyntax simple => simple.Body,
- ParenthesizedLambdaExpressionSyntax paren => paren.Body,
- AnonymousMethodExpressionSyntax anon => anon.Body,
- _ => null
- } is not { } body) {
- return false;
- }
-
- foreach (var assignment in body.DescendantNodesAndSelf().OfType()) {
- var leftText = assignment.Left.ToString();
- if (leftText.EndsWithIgnoreCase(".Endpoint") || leftText.EqualsOrdinal("Endpoint")) {
- return true;
- }
- }
-
- return false;
- }
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-
- private static bool HasEnvironmentVariableSet(InvocationExpressionSyntax invocation) {
- if (invocation.Ancestors().OfType().FirstOrDefault() is not { } containingMethod) {
- return false;
- }
-
- // Only check invocations that appear before this one in the method
- foreach (var inv in containingMethod.DescendantNodes().OfType().TakeWhile(inv => inv != invocation)) {
- if (GetMethodName(inv) is "SetEnvironmentVariable" or "Add"
- && inv.ArgumentList.Arguments is [{ Expression: LiteralExpressionSyntax literal }, ..]
- && literal.Token.ValueText.ContainsIgnoreCase("OTEL_EXPORTER_OTLP_ENDPOINT")) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0090UncompressedExportAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0090UncompressedExportAnalyzer.cs
deleted file mode 100644
index 906ffe2..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0090UncompressedExportAnalyzer.cs
+++ /dev/null
@@ -1,209 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0090: Detects OTLP exporter configurations using HTTP protocol without compression.
-///
-///
-///
-/// OTLP supports two transport protocols: gRPC and HTTP/protobuf. While gRPC
-/// automatically handles compression via its underlying HTTP/2 transport,
-/// HTTP/protobuf exports use uncompressed payloads by default.
-///
-///
-/// For services emitting large telemetry payloads (especially those using
-/// gen_ai.content attributes with full request/response text), enabling
-/// gzip compression can reduce bandwidth usage by 70-90% and significantly
-/// decrease export latency.
-///
-///
-/// The analyzer identifies OTLP exporter configurations that:
-/// 1. Explicitly set Protocol to HttpProtobuf without enabling compression
-/// 2. Use AddOtlpExporter/UseOtlpExporter without compression configuration
-///
-///
-/// gRPC protocol (OtlpExportProtocol.Grpc) is not flagged as it handles
-/// compression automatically.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0090UncompressedExportAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0090.
- private const string DiagnosticId = "AL0090";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverity.Warning);
-
- /// Method names that configure OTLP exporters.
- private static readonly HashSet s_otlpExporterMethods = [
- "AddOtlpExporter",
- "UseOtlpExporter",
- "WithOtlpExporter"
- ];
-
- /// Type names for OTLP exporter options.
- private static readonly string[] s_otlpOptionsTypeNames = [
- "OpenTelemetry.Exporter.OtlpExporterOptions",
- "OpenTelemetry.Exporter.OtlpExporterOptionsBase"
- ];
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers compilation start action to analyze OTLP exporter configurations.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var otlpOptionsTypes = s_otlpOptionsTypeNames
- .Select(context.Compilation.GetTypeByMetadataName)
- .WhereNotNull()
- .ToImmutableArray();
-
- if (otlpOptionsTypes.IsEmpty) {
- return;
- }
-
- var httpProtobufType = context.Compilation.GetTypeByMetadataName(
- "OpenTelemetry.Exporter.OtlpExportProtocol");
-
- context.RegisterSyntaxNodeAction(
- ctx => AnalyzeInvocation(ctx, otlpOptionsTypes, httpProtobufType),
- SyntaxKind.InvocationExpression);
- }
-
- private static void AnalyzeInvocation(
- SyntaxNodeAnalysisContext context,
- ImmutableArray otlpOptionsTypes,
- INamedTypeSymbol? httpProtobufType) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- if (GetMethodName(invocation) is not { } methodName || !s_otlpExporterMethods.Contains(methodName)) {
- return;
- }
-
- var lambdaArg = invocation.ArgumentList.Arguments
- .Select(a => a.Expression)
- .OfType()
- .FirstOrDefault();
-
- if (lambdaArg is not null) {
- if (!HasCompressionConfiguration(lambdaArg)
- && HasHttpProtobufConfiguration(lambdaArg, httpProtobufType, context.SemanticModel, context.CancellationToken)) {
- context.ReportDiagnostic(s_rule, GetMethodLocation(invocation));
- }
-
- return;
- }
-
- // Delegate arguments (AddOtlpExporter(ConfigureOtlp)) cannot be traced statically
- if (invocation.ArgumentList.Arguments.Any(a => a.Expression is IdentifierNameSyntax)) {
- return;
- }
-
- foreach (var arg in invocation.ArgumentList.Arguments) {
- if (ModelExtensions.GetTypeInfo(context.SemanticModel, arg.Expression, context.CancellationToken).Type is not { } argType) {
- continue;
- }
-
- if (otlpOptionsTypes.Any(optionsType => argType.InheritsFrom(optionsType) || argType.IsEqualTo(optionsType))
- && IsHttpProtobufOptionsWithoutCompression(arg.Expression)) {
- context.ReportDiagnostic(s_rule, GetMethodLocation(invocation));
- }
- }
- }
-
- private static bool HasCompressionConfiguration(SimpleLambdaExpressionSyntax lambda) {
- foreach (var node in lambda.DescendantNodes()) {
- var name = node switch {
- AssignmentExpressionSyntax assignment => GetMemberName(assignment.Left),
- InvocationExpressionSyntax invocation => GetMethodName(invocation),
- _ => null
- };
-
- if (name is not null && (name.ContainsIgnoreCase("compression") || name.ContainsIgnoreCase("gzip"))) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool HasHttpProtobufConfiguration(
- SimpleLambdaExpressionSyntax lambda,
- INamedTypeSymbol? httpProtobufType,
- SemanticModel semanticModel,
- CancellationToken cancellationToken) {
- foreach (var node in lambda.DescendantNodes()) {
- if (node is not AssignmentExpressionSyntax { Right: MemberAccessExpressionSyntax memberAccess } assignment
- || GetMemberName(assignment.Left) is not "Protocol") {
- continue;
- }
-
- if (memberAccess.Name.Identifier.Text.EqualsOrdinal("HttpProtobuf")) {
- return true;
- }
-
- // Fall back to semantic model for aliased/renamed references
- if (httpProtobufType is not null
- && ModelExtensions.GetSymbolInfo(semanticModel, memberAccess, cancellationToken).Symbol
- is IFieldSymbol { Name: "HttpProtobuf" } fieldSymbol
- && fieldSymbol.ContainingType.IsEqualTo(httpProtobufType)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool IsHttpProtobufOptionsWithoutCompression(
- ExpressionSyntax expression) {
- if (expression is not ObjectCreationExpressionSyntax { Initializer: { } initializer }) {
- return false;
- }
-
- var hasHttpProtobuf = false;
- var hasCompression = false;
-
- foreach (var expr in initializer.Expressions) {
- if (expr is not AssignmentExpressionSyntax assignment
- || GetMemberName(assignment.Left) is not { } leftText) {
- continue;
- }
-
- if (leftText.EqualsOrdinal("Protocol")
- && assignment.Right is MemberAccessExpressionSyntax { Name.Identifier.Text: "HttpProtobuf" }) {
- hasHttpProtobuf = true;
- }
-
- if (leftText.ContainsIgnoreCase("compression") || leftText.ContainsIgnoreCase("gzip")) {
- hasCompression = true;
- }
- }
-
- return hasHttpProtobuf && !hasCompression;
- }
-
- private static string? GetMemberName(ExpressionSyntax expression) =>
- expression switch {
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- _ => null
- };
-
- private static Location GetMethodLocation(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.GetLocation(),
- IdentifierNameSyntax identifier => identifier.GetLocation(),
- _ => invocation.GetLocation()
- };
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0091BatchExportDisabledAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0091BatchExportDisabledAnalyzer.cs
deleted file mode 100644
index cca3589..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0091BatchExportDisabledAnalyzer.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0091: Detects usage of SimpleSpanProcessor or SimpleActivityExportProcessor which exports
-/// spans one at a time instead of batching.
-///
-///
-///
-/// SimpleSpanProcessor and SimpleActivityExportProcessor export each span immediately as it
-/// completes, which can cause significant performance overhead in production applications.
-///
-///
-/// BatchSpanProcessor and BatchActivityExportProcessor collect spans and export them in
-/// configurable batches, reducing network overhead and improving application performance.
-///
-///
-/// Simple processors are useful for debugging and development scenarios where immediate
-/// export is desired, but batch processors are recommended for production use.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0091BatchExportDisabledAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0091.
- private const string DiagnosticId = "AL0091";
-
- private static readonly string[] s_simpleProcessorTypeNames = [
- "OpenTelemetry.Trace.SimpleSpanProcessor",
- "OpenTelemetry.Trace.SimpleActivityExportProcessor",
- "SimpleSpanProcessor",
- "SimpleActivityExportProcessor"
- ];
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers operation actions to analyze object creation.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
-
- private static void AnalyzeObjectCreation(OperationAnalysisContext context) {
- if (context.Operation is not IObjectCreationOperation { Type: { } type }) {
- return;
- }
-
- if (IsSimpleProcessor(type.ToDisplayString())) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, context.Operation.Syntax.GetLocation()));
- }
- }
-
- private static bool IsSimpleProcessor(string typeName) =>
- s_simpleProcessorTypeNames.Any(s => typeName.EndsWithOrdinal(s) || typeName.EqualsOrdinal(s));
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0092ConsiderSamplingAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0092ConsiderSamplingAnalyzer.cs
deleted file mode 100644
index 0f7671d..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0092ConsiderSamplingAnalyzer.cs
+++ /dev/null
@@ -1,172 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0092: Detects OpenTelemetry tracing configurations without sampling configured.
-///
-///
-///
-/// High-volume services can generate excessive telemetry data without proper
-/// sampling configuration. This analyzer flags tracing configurations that:
-///
-///
-/// - Do not call SetSampler at all (uses default AlwaysOnSampler)
-/// - Explicitly use AlwaysOnSampler (captures every span)
-///
-///
-/// For production environments, consider using:
-///
-///
-/// - ParentBasedSampler - Respects parent sampling decisions
-/// - TraceIdRatioBasedSampler - Probabilistic sampling
-/// - Tail-based sampling at the collector level
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0092ConsiderSamplingAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0092.
- private const string DiagnosticId = "AL0092";
-
- /// Array of known OTel tracer builder type names.
- private static readonly string[] s_tracerBuilderTypeNames = [
- "OpenTelemetry.Trace.TracerProviderBuilder",
- "OpenTelemetry.IOpenTelemetryBuilder"
- ];
-
- /// Methods that configure tracing and should have sampling configured.
- private static readonly HashSet s_tracingConfigMethods = [
- "WithTracing",
- "AddTracing",
- "ConfigureTracing"
- ];
-
- /// Sampler types that capture all spans (problematic for high-volume services).
- private static readonly HashSet s_alwaysOnSamplerTypes = [
- "AlwaysOnSampler",
- "OpenTelemetry.Trace.AlwaysOnSampler"
- ];
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.HiddenByDefault);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers compilation start action to analyze tracing configurations.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var tracerBuilderTypes = s_tracerBuilderTypeNames
- .Select(context.Compilation.GetTypeByMetadataName)
- .WhereNotNull()
- .ToImmutableArray();
-
- if (tracerBuilderTypes.IsEmpty) {
- return;
- }
-
- context.RegisterSyntaxNodeAction(
- ctx => AnalyzeInvocation(ctx, tracerBuilderTypes),
- SyntaxKind.InvocationExpression);
- }
-
- private static void AnalyzeInvocation(
- SyntaxNodeAnalysisContext context,
- ImmutableArray tracerBuilderTypes) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- var methodName = GetMethodName(invocation);
- if (methodName is null || !s_tracingConfigMethods.Contains(methodName)) {
- return;
- }
-
- if (!IsTracerBuilderCall(invocation, context.SemanticModel, tracerBuilderTypes, context.CancellationToken)) {
- return;
- }
-
- if (HasSamplerConfiguration(invocation, out var usesAlwaysOn)) {
- if (usesAlwaysOn) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, GetMethodLocation(invocation)));
- }
-
- return;
- }
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, GetMethodLocation(invocation)));
- }
-
- private static bool IsTracerBuilderCall(
- InvocationExpressionSyntax invocation,
- SemanticModel semanticModel,
- ImmutableArray tracerBuilderTypes,
- CancellationToken cancellationToken) {
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess
- || ModelExtensions.GetTypeInfo(semanticModel, memberAccess.Expression, cancellationToken).Type is not { } receiverType) {
- return false;
- }
-
- return tracerBuilderTypes.Any(builderType =>
- receiverType.InheritsFrom(builderType) || receiverType.Implements(builderType));
- }
-
- private static bool HasSamplerConfiguration(SyntaxNode invocation, out bool usesAlwaysOnSampler) {
- usesAlwaysOnSampler = false;
-
- foreach (var node in invocation.DescendantNodes()) {
- switch (node) {
- case InvocationExpressionSyntax nestedInvocation: {
- var nestedMethod = GetMethodName(nestedInvocation);
- if (nestedMethod?.EqualsOrdinal("SetSampler") == true) {
- usesAlwaysOnSampler = CheckForAlwaysOnSampler(nestedInvocation);
- return true;
- }
-
- break;
- }
- case IdentifierNameSyntax identifier: {
- var name = identifier.Identifier.Text;
- if (s_alwaysOnSamplerTypes.Any(s => s.ContainsIgnoreCase(name))) {
- usesAlwaysOnSampler = true;
- }
-
- break;
- }
- }
- }
-
- return false;
- }
-
- private static bool CheckForAlwaysOnSampler(InvocationExpressionSyntax setSamplerInvocation) {
- foreach (var descendant in setSamplerInvocation.DescendantNodes()) {
- switch (descendant) {
- case IdentifierNameSyntax identifier
- when s_alwaysOnSamplerTypes.Contains(identifier.Identifier.Text):
- return true;
- case ObjectCreationExpressionSyntax { Type: IdentifierNameSyntax typeName } when s_alwaysOnSamplerTypes.Contains(typeName.Identifier.Text):
- return true;
- case ObjectCreationExpressionSyntax { Type: QualifiedNameSyntax qualifiedName } when s_alwaysOnSamplerTypes.Contains(qualifiedName.ToString()):
- return true;
- }
- }
-
- return false;
- }
-
- private static Location GetMethodLocation(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.GetLocation(),
- IdentifierNameSyntax identifier => identifier.GetLocation(),
- _ => invocation.GetLocation()
- };
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0093MissingResourceAttributesAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0093MissingResourceAttributesAnalyzer.cs
deleted file mode 100644
index f041d5f..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0093MissingResourceAttributesAnalyzer.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0093: Detects when OpenTelemetry is configured without essential resource attributes.
-///
-///
-///
-/// Resource attributes like 'service.name' and 'service.version' are critical for
-/// identifying services in observability backends. Without them, traces and metrics
-/// appear with generic or missing service identification, making it difficult to
-/// correlate telemetry across services.
-///
-///
-/// This analyzer detects calls to AddOpenTelemetry() or similar configuration methods
-/// and checks if ConfigureResource() or AddService() is called to set the service identity.
-///
-///
-/// The fix is to add ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion)
-/// or use ConfigureResource() with appropriate service attributes.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0093MissingResourceAttributesAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0093.
- private const string DiagnosticId = "AL0093";
-
- private static readonly string[] s_oTelSetupMethods = [
- "AddOpenTelemetry",
- "UseOpenTelemetry",
- "ConfigureOpenTelemetry"
- ];
-
- private static readonly string[] s_resourceConfigMethods = [
- "ConfigureResource",
- "AddResource",
- "AddService",
- "SetResourceBuilder"
- ];
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers syntax node actions to analyze OpenTelemetry configuration.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
-
- private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) {
- var invocation = (InvocationExpressionSyntax)context.Node;
-
- var methodName = GetMethodName(invocation);
- if (methodName is null || !s_oTelSetupMethods.Contains(methodName)) {
- return;
- }
-
- if (invocation.Ancestors().OfType().FirstOrDefault() is not { } containingMethod) {
- return;
- }
-
- var allInvocations = new HashSet();
- foreach (var inv in containingMethod.DescendantNodes().OfType()) {
- if (GetMethodName(inv) is { } name) {
- allInvocations.Add(name);
- }
- }
-
- if (!s_resourceConfigMethods.Any(allInvocations.Contains)) {
- context.ReportDiagnostic(Diagnostic.Create(s_rule, GetMethodNameLocation(invocation), "service.name/service.version"));
- }
- }
-
- private static string? GetMethodName(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
- IdentifierNameSyntax identifier => identifier.Identifier.Text,
- _ => null
- };
-
- private static Location GetMethodNameLocation(InvocationExpressionSyntax invocation) =>
- invocation.Expression switch {
- MemberAccessExpressionSyntax memberAccess => memberAccess.Name.GetLocation(),
- IdentifierNameSyntax identifier => identifier.GetLocation(),
- _ => invocation.GetLocation()
- };
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0096EnableEventSourceSupportAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0096EnableEventSourceSupportAnalyzer.cs
deleted file mode 100644
index ccc722f..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0096EnableEventSourceSupportAnalyzer.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0096: Enable EventSourceSupport for AOT with telemetry.
-///
-///
-///
-/// Native AOT trims EventSource/EventPipe infrastructure by default.
-/// If the application uses OpenTelemetry, dotnet-trace, dotnet-counters, or other
-/// EventPipe-based diagnostics, <EventSourceSupport>true</EventSourceSupport>
-/// must be set in the project file to preserve this infrastructure.
-///
-///
-/// This analyzer checks MSBuild properties exposed via CompilerVisibleProperty to detect
-/// when PublishAot is true but EventSourceSupport is not explicitly enabled.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0096EnableEventSourceSupportAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0096.
- private const string DiagnosticId = "AL0096";
-
- private const string PublishAotProperty = "build_property.PublishAot";
- private const string EventSourceSupportProperty = "build_property.EventSourceSupport";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.Configuration,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers a compilation action to check MSBuild properties.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationAction(AnalyzeCompilation);
-
- private static void AnalyzeCompilation(CompilationAnalysisContext context) {
- var globalOptions = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions;
-
- if (!globalOptions.TryGetValue(PublishAotProperty, out var publishAot)
- || !string.Equals(publishAot, "true", StringComparison.OrdinalIgnoreCase)) {
- return;
- }
-
- if (globalOptions.TryGetValue(EventSourceSupportProperty, out var eventSourceSupport)
- && string.Equals(eventSourceSupport, "true", StringComparison.OrdinalIgnoreCase)) {
- return;
- }
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, Location.None));
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0107OrphanedTracedTagAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0107OrphanedTracedTagAnalyzer.cs
deleted file mode 100644
index 25ca3fc..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0107OrphanedTracedTagAnalyzer.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0107: Detects [TracedTag] on parameters where neither the method nor its declaring type has [Traced].
-///
-///
-///
-/// The [TracedTag] attribute is only meaningful when used on parameters of methods that
-/// participate in tracing via [Traced]. When neither the method nor its declaring type
-/// (including base types) has [Traced], the tag will be silently ignored:
-///
-/// - No span is created, so there is nothing to attach the tag to
-/// - The attribute becomes dead metadata with no runtime effect
-/// - It may mislead developers into thinking tags are being recorded
-///
-///
-///
-/// Example of problematic code:
-///
-/// public class OrderService {
-/// // No [Traced] on class or method
-/// public void Process([TracedTag] string orderId) { }
-/// }
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0107OrphanedTracedTagAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0107.
- public const string DiagnosticId = "AL0107";
-
- private const string TracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedAttribute";
- private const string TracedTagAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedTagAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze method parameters for orphaned [TracedTag].
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- var tracedType = compilationContext.Compilation.GetTypeByMetadataName(TracedAttributeFullName);
- var tracedTagType = compilationContext.Compilation.GetTypeByMetadataName(TracedTagAttributeFullName);
-
- if (tracedType is null || tracedTagType is null) {
- return;
- }
-
- compilationContext.RegisterSymbolAction(
- ctx => AnalyzeMethod(ctx, tracedType, tracedTagType),
- SymbolKind.Method);
- });
- }
-
- private static void AnalyzeMethod(
- SymbolAnalysisContext context,
- INamedTypeSymbol tracedType,
- INamedTypeSymbol tracedTagType) {
- var method = (IMethodSymbol)context.Symbol;
-
- if (method.HasAttribute(tracedType) || HasTracedOnType(method.ContainingType, tracedType)) {
- return;
- }
-
- foreach (var param in method.Parameters) {
- if (param.HasAttribute(tracedTagType)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- param.Locations.FirstOrDefault() ?? Location.None,
- param.Name));
- }
- }
- }
-
- private static bool HasTracedOnType(INamedTypeSymbol? type, INamedTypeSymbol tracedType) {
- for (var current = type; current is not null; current = current.BaseType) {
- if (current.HasAttribute(tracedType)) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0108RedundantNoTraceAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0108RedundantNoTraceAnalyzer.cs
deleted file mode 100644
index cd6d7b2..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0108RedundantNoTraceAnalyzer.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0108: Detects [NoTrace] on a method whose declaring type has no class-level [Traced].
-///
-///
-///
-/// The [NoTrace] attribute is used to opt a method out of class-level [Traced]
-/// auto-instrumentation. When the declaring type (including base types) does not
-/// have [Traced], the [NoTrace] attribute has no effect:
-///
-/// - There is no class-level tracing to opt out of
-/// - The attribute becomes misleading dead metadata
-/// - It may confuse developers about the instrumentation scope
-///
-///
-///
-/// Example of problematic code:
-///
-/// public class OrderService {
-/// // No [Traced] on class
-/// [NoTrace] // Redundant!
-/// public void HelperMethod() { }
-/// }
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0108RedundantNoTraceAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0108.
- public const string DiagnosticId = "AL0108";
-
- private const string TracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedAttribute";
- private const string NoTraceAttributeFullName = "Qyl.Instrumentation.Instrumentation.NoTraceAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.HiddenByDefault);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze methods for redundant [NoTrace].
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- var tracedType = compilationContext.Compilation.GetTypeByMetadataName(TracedAttributeFullName);
- var noTraceType = compilationContext.Compilation.GetTypeByMetadataName(NoTraceAttributeFullName);
-
- if (tracedType is null || noTraceType is null) {
- return;
- }
-
- compilationContext.RegisterSymbolAction(
- ctx => AnalyzeMethod(ctx, tracedType, noTraceType),
- SymbolKind.Method);
- });
- }
-
- private static void AnalyzeMethod(
- SymbolAnalysisContext context,
- INamedTypeSymbol tracedType,
- INamedTypeSymbol noTraceType) {
- var method = (IMethodSymbol)context.Symbol;
-
- if (method.HasAttribute(noTraceType) && !HasTracedOnType(method.ContainingType, tracedType)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- method.Locations.FirstOrDefault() ?? Location.None,
- method.Name));
- }
- }
-
- private static bool HasTracedOnType(INamedTypeSymbol? type, INamedTypeSymbol tracedType) {
- for (var current = type; current is not null; current = current.BaseType) {
- if (current.HasAttribute(tracedType)) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0109NonInterceptableTracedAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0109NonInterceptableTracedAnalyzer.cs
deleted file mode 100644
index 96163dc..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0109NonInterceptableTracedAnalyzer.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0109: Detects [Traced] on abstract, extern, or partial definition methods that cannot be intercepted.
-///
-///
-///
-/// The [Traced] attribute on a method triggers compile-time interception to create spans.
-/// Certain method kinds cannot be intercepted by the source generator:
-///
-/// - Abstract methods: Have no implementation to intercept
-/// - Extern methods: Implemented externally (P/Invoke)
-/// - Partial definitions: Only the definition half, no body to wrap
-///
-///
-///
-/// The [Traced] attribute will be silently ignored on these methods, which may
-/// mislead developers into thinking spans are being created.
-///
-///
-/// Example of problematic code:
-///
-/// public abstract class BaseService {
-/// [Traced("MyApp")]
-/// public abstract Task ProcessAsync(); // Cannot be intercepted!
-/// }
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0109NonInterceptableTracedAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0109.
- public const string DiagnosticId = "AL0109";
-
- private const string TracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze methods with [Traced] for interceptability.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- if (compilationContext.Compilation.GetTypeByMetadataName(TracedAttributeFullName) is not { } tracedType) {
- return;
- }
-
- compilationContext.RegisterSymbolAction(
- ctx => AnalyzeMethod(ctx, tracedType),
- SymbolKind.Method);
- });
- }
-
- private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol tracedType) {
- if (context.Symbol is not IMethodSymbol { IsAbstract: true } and not IMethodSymbol { IsExtern: true } and not IMethodSymbol { IsPartialDefinition: true }) {
- return;
- }
-
- var method = (IMethodSymbol)context.Symbol;
-
- if (method.HasAttribute(tracedType)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- method.Locations.FirstOrDefault() ?? Location.None,
- method.Name));
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0110TracedTagOnOutRefParameterAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0110TracedTagOnOutRefParameterAnalyzer.cs
deleted file mode 100644
index de39713..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0110TracedTagOnOutRefParameterAnalyzer.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0110: Detects [TracedTag] on parameters with out or ref modifiers.
-///
-///
-///
-/// The [TracedTag] attribute records parameter values as span attributes at the
-/// start of method execution. Parameters with out or ref modifiers
-/// are problematic because:
-///
-/// - out parameters: Have no meaningful value at method entry
-/// - ref parameters: May be mutated during execution, making the recorded value misleading
-/// - The interceptor captures values before the method body executes
-///
-///
-///
-/// Example of problematic code:
-///
-/// [Traced("MyApp")]
-/// public bool TryParse([TracedTag] string input, [TracedTag] out int result) {
-/// // 'result' has no value when the tag would be recorded
-/// }
-///
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0110TracedTagOnOutRefParameterAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0110.
- public const string DiagnosticId = "AL0110";
-
- private const string TracedTagAttributeFullName = "Qyl.Instrumentation.Instrumentation.TracedTagAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverities.RequiredFix);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze method parameters for [TracedTag] on out/ref.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- if (compilationContext.Compilation.GetTypeByMetadataName(TracedTagAttributeFullName) is not { } tracedTagType) {
- return;
- }
-
- compilationContext.RegisterSymbolAction(
- ctx => AnalyzeMethod(ctx, tracedTagType),
- SymbolKind.Method);
- });
- }
-
- private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol tracedTagType) {
- var method = (IMethodSymbol)context.Symbol;
-
- foreach (var param in method.Parameters) {
- if (param.RefKind is RefKind.Out or RefKind.Ref &&
- param.GetAttributes().Any(a => a.AttributeClass.IsEqualTo(tracedTagType))) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- param.Locations.FirstOrDefault() ?? Location.None,
- param.Name));
- }
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0113MissingExceptionRecordingOnActivityAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0113MissingExceptionRecordingOnActivityAnalyzer.cs
deleted file mode 100644
index b342c01..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0113MissingExceptionRecordingOnActivityAnalyzer.cs
+++ /dev/null
@@ -1,202 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0113: Detects Activity.SetStatus(Error) inside catch blocks without recording the exception.
-///
-///
-///
-/// When setting an Activity status to Error in a catch block, the exception should be
-/// recorded as an event on the span for proper observability. Without the exception event,
-/// traces show that an error occurred but provide no details about what went wrong.
-///
-///
-/// The fix is to call activity.AddEvent(new ActivityEvent("exception", ...)) or
-/// a RecordException helper before or after the SetStatus call.
-///
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0113MissingExceptionRecordingOnActivityAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0113.
- private const string DiagnosticId = "AL0113";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverity.Warning);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers a compilation-start action to resolve Activity and ActivityStatusCode types.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var activityType = context.Compilation.GetTypeByMetadataName("System.Diagnostics.Activity");
- var statusCodeType = context.Compilation.GetTypeByMetadataName("System.Diagnostics.ActivityStatusCode");
-
- if (activityType is null || statusCodeType is null) {
- return;
- }
-
- context.RegisterOperationAction(
- ctx => AnalyzeInvocation(ctx, activityType, statusCodeType),
- OperationKind.Invocation);
- }
-
- private static void AnalyzeInvocation(
- OperationAnalysisContext context,
- INamedTypeSymbol activityType,
- INamedTypeSymbol statusCodeType) {
- var invocation = (IInvocationOperation)context.Operation;
-
- if (!IsSetStatusErrorOnActivity(invocation, activityType, statusCodeType)) {
- return;
- }
-
- if (FindEnclosingCatchClause(invocation) is not { } catchClause) {
- return;
- }
-
- var activityInstance = GetReceiverIdentifier(invocation);
-
- if (HasExceptionRecording(catchClause, activityInstance)) {
- return;
- }
-
- context.ReportDiagnostic(Diagnostic.Create(s_rule, invocation.Syntax.GetLocation()));
- }
-
- private static bool IsSetStatusErrorOnActivity(
- IInvocationOperation invocation,
- INamedTypeSymbol activityType,
- INamedTypeSymbol statusCodeType) {
- if (invocation.TargetMethod.Name != "SetStatus") {
- return false;
- }
-
- if (!SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, activityType)) {
- return false;
- }
-
- if (invocation.Arguments.Length is 0) {
- return false;
- }
-
- var firstArg = invocation.Arguments[0].Value;
- while (firstArg is IConversionOperation conversion) {
- firstArg = conversion.Operand;
- }
-
- if (firstArg is not IFieldReferenceOperation fieldRef) {
- return false;
- }
-
- return SymbolEqualityComparer.Default.Equals(fieldRef.Field.ContainingType, statusCodeType) &&
- fieldRef.Field.Name == "Error";
- }
-
- private static IOperation? FindEnclosingCatchClause(IOperation operation) {
- var current = operation.Parent;
- while (current is not null) {
- switch (current) {
- case ICatchClauseOperation:
- return current;
- case IMethodBodyOperation:
- case IAnonymousFunctionOperation:
- case ILocalFunctionOperation:
- return null;
- }
-
- current = current.Parent;
- }
-
- return null;
- }
-
- private static string? GetReceiverIdentifier(IInvocationOperation invocation) =>
- invocation.Instance switch {
- ILocalReferenceOperation local => local.Local.Name,
- IParameterReferenceOperation param => param.Parameter.Name,
- IFieldReferenceOperation field => field.Field.Name,
- _ => null
- };
-
- private static bool HasExceptionRecording(IOperation catchClause, string? activityIdentifier) =>
- SearchForExceptionRecording(catchClause, activityIdentifier);
-
- private static bool SearchForExceptionRecording(IOperation operation, string? activityIdentifier) {
- switch (operation) {
- case IInvocationOperation invocation:
- if (IsExceptionRecordingCall(invocation, activityIdentifier)) {
- return true;
- }
-
- break;
- }
-
- foreach (var child in operation.ChildOperations) {
- if (SearchForExceptionRecording(child, activityIdentifier)) {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool IsExceptionRecordingCall(IInvocationOperation invocation, string? activityIdentifier) {
- var methodName = invocation.TargetMethod.Name;
-
- switch (methodName) {
- case "RecordException":
- case "AddException":
- return activityIdentifier is null || MatchesReceiver(invocation, activityIdentifier);
-
- case "AddEvent":
- if (activityIdentifier is not null && !MatchesReceiver(invocation, activityIdentifier)) {
- return false;
- }
-
- return HasExceptionEventArgument(invocation);
-
- default:
- return false;
- }
- }
-
- private static bool MatchesReceiver(IInvocationOperation invocation, string activityIdentifier) =>
- invocation.Instance switch {
- ILocalReferenceOperation local => local.Local.Name == activityIdentifier,
- IParameterReferenceOperation param => param.Parameter.Name == activityIdentifier,
- IFieldReferenceOperation field => field.Field.Name == activityIdentifier,
- _ => false
- };
-
- private static bool HasExceptionEventArgument(IInvocationOperation invocation) {
- if (invocation.Arguments.Length is 0) {
- return false;
- }
-
- var arg = invocation.Arguments[0].Value;
- while (arg is IConversionOperation conversion) {
- arg = conversion.Operand;
- }
-
- if (arg is not IObjectCreationOperation creation) {
- return true;
- }
-
- if (creation.Arguments.Length is 0) {
- return false;
- }
-
- var nameArg = creation.Arguments[0].Value;
- while (nameArg is IConversionOperation nameConversion) {
- nameArg = nameConversion.Operand;
- }
-
- return nameArg.ConstantValue is { HasValue: true, Value: string eventName } &&
- eventName.EqualsIgnoreCase("exception");
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0124NonInterceptableAgentTracedAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0124NonInterceptableAgentTracedAnalyzer.cs
deleted file mode 100644
index 8a7d6ad..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0124NonInterceptableAgentTracedAnalyzer.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0124: Detects [AgentTraced] on abstract, extern, or partial definition methods that cannot be intercepted.
-///
-///
-/// The [AgentTraced] attribute on a method triggers compile-time interception to create agent spans.
-/// Certain method kinds cannot be intercepted by the source generator:
-///
-/// - Abstract methods: Have no implementation to intercept
-/// - Extern methods: Implemented externally (P/Invoke)
-/// - Partial definitions: Only the definition half, no body to wrap
-///
-/// The [AgentTraced] attribute will be silently ignored on these methods, which may
-/// mislead developers into thinking agent spans are being created.
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0124NonInterceptableAgentTracedAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0124.
- public const string DiagnosticId = "AL0124";
-
- private const string AgentTracedAttributeFullName = "Qyl.Instrumentation.Instrumentation.AgentTracedAttribute";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.GenAI,
- DiagnosticSeverities.Suggestion);
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers symbol actions to analyze methods with [AgentTraced] for interceptability.
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- if (compilationContext.Compilation.GetTypeByMetadataName(AgentTracedAttributeFullName) is not { } agentTracedType) {
- return;
- }
-
- compilationContext.RegisterSymbolAction(
- ctx => AnalyzeMethod(ctx, agentTracedType),
- SymbolKind.Method);
- });
- }
-
- private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol agentTracedType) {
- if (context.Symbol is not IMethodSymbol { IsAbstract: true } and not IMethodSymbol { IsExtern: true } and not IMethodSymbol { IsPartialDefinition: true }) {
- return;
- }
-
- var method = (IMethodSymbol)context.Symbol;
-
- if (method.HasAttribute(agentTracedType)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- method.Locations.FirstOrDefault() ?? Location.None,
- method.Name));
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0131DirectGenAiSdkUsageAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0131DirectGenAiSdkUsageAnalyzer.cs
deleted file mode 100644
index b73b339..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0131DirectGenAiSdkUsageAnalyzer.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0131: Warns when application code calls GenAI SDK APIs directly, bypassing the
-/// IChatClient abstraction and the automatic OpenTelemetry instrumentation pipeline.
-///
-///
-/// Register the SDK client through DI and layer instrumentation on the resulting
-/// IChatClient:
-///
-/// builder.Services.AddChatClient(inner => new OpenAIChatClient(model, apiKey))
-/// .AsBuilder()
-/// .UseFunctionInvocation()
-/// .UseOpenTelemetry(sourceName)
-/// .Build();
-///
-/// Direct SDK calls — OpenAI.Chat.ChatClient.CompleteChatAsync, Anthropic.AnthropicClient.CreateMessageAsync, etc.
-/// — bypass this pipeline and emit no telemetry.
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0131DirectGenAiSdkUsageAnalyzer : AlAnalyzer {
- /// The diagnostic identifier for AL0131.
- private const string DiagnosticId = "AL0131";
-
- private const string IChatClientMetadataName = "Microsoft.Extensions.AI.IChatClient";
-
- ///
- /// Direct-SDK type metadata names paired with a short display label. IChatClient is
- /// intentionally absent — it is the instrumented abstraction. Likewise AIAgent,
- /// ChatClientAgent, and DelegatingAIAgent are NOT bypasses — they are the
- /// Microsoft.Agents.AI abstractions the framework expects consumers to call through.
- ///
- private static readonly (string MetadataName, string Label)[] s_sdkTypes = [
- ("OpenAI.Chat.ChatClient", "OpenAI Chat"),
- ("OpenAI.Embeddings.EmbeddingClient", "OpenAI Embeddings"),
- ("OpenAI.Images.ImageClient", "OpenAI Images"),
- ("OpenAI.Audio.AudioClient", "OpenAI Audio"),
- ("Anthropic.AnthropicClient", "Anthropic"),
- ("Anthropic.Messaging.MessageClient", "Anthropic Messaging"),
- ("OllamaSharp.OllamaApiClient", "Ollama"),
- ("Azure.AI.OpenAI.OpenAIClient", "Azure.AI.OpenAI"),
- ("Cohere.CohereClient", "Cohere")
- ];
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.GenAI,
- DiagnosticSeverities.Suggestion);
-
- ///
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- ///
- protected override void RegisterActions(AnalysisContext context) {
- context.RegisterCompilationStartAction(compilationContext => {
- var iChatClientType = compilationContext.Compilation.GetTypeByMetadataName(IChatClientMetadataName);
-
- var knownSdkTypes = s_sdkTypes
- .Select(entry => (
- Type: compilationContext.Compilation.GetTypeByMetadataName(entry.MetadataName),
- entry.Label))
- .Where(entry => entry.Type is not null)
- .ToImmutableArray();
-
- if (knownSdkTypes.IsEmpty) {
- return;
- }
-
- compilationContext.RegisterOperationAction(
- ctx => AnalyzeInvocation(ctx, iChatClientType, knownSdkTypes),
- OperationKind.Invocation);
- });
- }
-
- private static void AnalyzeInvocation(
- OperationAnalysisContext context,
- INamedTypeSymbol? iChatClientType,
- ImmutableArray<(INamedTypeSymbol? Type, string Label)> knownSdkTypes) {
- var invocation = (IInvocationOperation)context.Operation;
- if (invocation.TargetMethod.ContainingType is not { } containingType) {
- return;
- }
-
- if (iChatClientType is not null && containingType.IsEqualTo(iChatClientType)) {
- return;
- }
-
- foreach (var (sdkType, label) in knownSdkTypes) {
- if (sdkType is not null && containingType.IsEqualTo(sdkType)) {
- context.ReportDiagnostic(Diagnostic.Create(
- s_rule,
- invocation.Syntax.GetLocation(),
- label));
- return;
- }
- }
- }
-}
diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0135LegacySemanticConventionsAccessorAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0135LegacySemanticConventionsAccessorAnalyzer.cs
deleted file mode 100644
index 7627678..0000000
--- a/src/ANcpLua.Analyzers/Analyzers/AL0135LegacySemanticConventionsAccessorAnalyzer.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// AL0135: Flags usage of the legacy aggregated semantic-convention accessor types
-/// (SemanticConventions, TraceSemanticConventions, ResourceSemanticConventions)
-/// and steers callers toward the per-domain grouped classes published under
-/// OpenTelemetry.SemanticConventions.Attributes.*.
-///
-///
-/// The aggregated classes were the initial surface shape of the
-/// OpenTelemetry.SemanticConventions package. Newer releases split the catalog
-/// into one static class per convention namespace (HttpAttributes, ServerAttributes,
-/// DbAttributes, …). Keeping code on the aggregator blocks structured refactors on
-/// convention upgrades and produces noisy diffs because every convention change lands
-/// in the same file. No code fix is offered because the legacy-to-grouped field
-/// mapping is not a simple rename (for example AttributeHttpMethod →
-/// HttpAttributes.AttributeHttpRequestMethod); operators must choose the right
-/// grouped class per field.
-///
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed partial class Al0135LegacySemanticConventionsAccessorAnalyzer : AlAnalyzer {
- private const string DiagnosticId = "AL0135";
-
- private static readonly DiagnosticDescriptor s_rule = CreateRule(
- DiagnosticId,
- DiagnosticCategories.OpenTelemetry,
- DiagnosticSeverity.Warning);
-
- private static readonly string[] s_legacyAccessorTypes = [
- "OpenTelemetry.SemanticConventions.SemanticConventions",
- "OpenTelemetry.Trace.TraceSemanticConventions",
- "OpenTelemetry.Resource.ResourceSemanticConventions",
- "OpenTelemetry.Resources.ResourceSemanticConventions"
- ];
-
- private const string GroupedReplacement = "OpenTelemetry.SemanticConventions.Attributes.*Attributes";
-
- /// Gets the diagnostic descriptors for the supported diagnostics.
- public override ImmutableArray SupportedDiagnostics => [s_rule];
-
- /// Registers a compilation-start action that resolves legacy accessor types.
- protected override void RegisterActions(AnalysisContext context) =>
- context.RegisterCompilationStartAction(OnCompilationStart);
-
- private static void OnCompilationStart(CompilationStartAnalysisContext context) {
- var legacyTypes = s_legacyAccessorTypes
- .Select(context.Compilation.GetTypeByMetadataName)
- .WhereNotNull()
- .ToImmutableArray();
-
- if (legacyTypes.IsEmpty) {
- return;
- }
-
- context.RegisterOperationAction(
- ctx => AnalyzeFieldReference(ctx, legacyTypes),
- OperationKind.FieldReference);
- }
-
- private static void AnalyzeFieldReference(
- OperationAnalysisContext context,
- ImmutableArray legacyTypes) {
- var fieldReference = (IFieldReferenceOperation)context.Operation;
-
- if (fieldReference.Field.ContainingType is not { } container
- || !legacyTypes.Any(container.IsEqualTo)) {
- return;
- }
-
- var qualifiedMember = $"{container.Name}.{fieldReference.Field.Name}";
- context.ReportDiagnostic(s_rule, fieldReference.Syntax.GetLocation(), qualifiedMember, GroupedReplacement);
- }
-}
diff --git a/src/ANcpLua.Analyzers/Semconv/OpenTelemetryDeprecatedSemconvCatalog.cs b/src/ANcpLua.Analyzers/Semconv/OpenTelemetryDeprecatedSemconvCatalog.cs
deleted file mode 100644
index 3bd870c..0000000
--- a/src/ANcpLua.Analyzers/Semconv/OpenTelemetryDeprecatedSemconvCatalog.cs
+++ /dev/null
@@ -1,253 +0,0 @@
-namespace ANcpLua.Analyzers.Analyzers;
-
-///
-/// Central snapshot of deprecated OpenTelemetry semantic conventions sourced from
-/// semantic-conventions/model/**/*.yaml.
-///
-internal static class OpenTelemetryDeprecatedSemconvCatalog {
- private static readonly Dictionary s_deprecatedAttributes =
- new(StringComparer.OrdinalIgnoreCase) {
- ["android.state"] = ("android.app.state", "1.31.0"),
- ["az.namespace"] = ("azure.resource_provider.namespace", "1.35.0"),
- ["az.service_request_id"] = ("azure.service.request.id", "1.35.0"),
- ["code.column"] = ("code.column.number", "1.30.0"),
- ["code.filepath"] = ("code.file.path", "1.30.0"),
- ["code.lineno"] = ("code.line.number", "1.30.0"),
- ["container.cpu.state"] = ("cpu.mode", "1.27.0"),
- ["container.labels"] = ("container.label", "1.25.0"),
- ["container.runtime"] = ("container.runtime.name", "1.37.0"),
- ["db.cassandra.consistency_level"] = ("cassandra.consistency.level", "1.30.0"),
- ["db.cassandra.coordinator.dc"] = ("cassandra.coordinator.dc", "1.30.0"),
- ["db.cassandra.coordinator.id"] = ("cassandra.coordinator.id", "1.30.0"),
- ["db.cassandra.idempotence"] = ("cassandra.query.idempotent", "1.30.0"),
- ["db.cassandra.page_size"] = ("cassandra.page.size", "1.30.0"),
- ["db.cassandra.speculative_execution_count"] = ("cassandra.speculative_execution.count", "1.30.0"),
- ["db.cassandra.table"] = ("db.collection.name", "1.25.0"),
- ["db.client.connections.pool.name"] = ("db.client.connection.pool.name", "1.27.0"),
- ["db.client.connections.state"] = ("db.client.connection.state", "1.27.0"),
- ["db.cosmosdb.client_id"] = ("azure.client.id", "1.30.0"),
- ["db.cosmosdb.connection_mode"] = ("azure.cosmosdb.connection.mode", "1.30.0"),
- ["db.cosmosdb.consistency_level"] = ("azure.cosmosdb.consistency.level", "1.30.0"),
- ["db.cosmosdb.container"] = ("db.collection.name", "1.25.0"),
- ["db.cosmosdb.regions_contacted"] = ("azure.cosmosdb.operation.contacted_regions", "1.30.0"),
- ["db.cosmosdb.request_charge"] = ("azure.cosmosdb.operation.request_charge", "1.30.0"),
- ["db.cosmosdb.request_content_length"] = ("azure.cosmosdb.request.body.size", "1.30.0"),
- ["db.cosmosdb.sub_status_code"] = ("azure.cosmosdb.response.sub_status_code", "1.30.0"),
- ["db.elasticsearch.cluster.name"] = ("db.namespace", "1.27.0"),
- ["db.elasticsearch.node.name"] = ("elasticsearch.node.name", "1.30.0"),
- ["db.mongodb.collection"] = ("db.collection.name", "1.25.0"),
- ["db.name"] = ("db.namespace", "1.25.0"),
- ["db.operation"] = ("db.operation.name", "1.25.0"),
- ["db.statement"] = ("db.query.text", "1.25.0"),
- ["db.system"] = ("db.system.name", "1.30.0"),
- ["deployment.environment"] = ("deployment.environment.name", "1.27.0"),
- ["feature_flag.evaluation.error.message"] = ("feature_flag.error.message", "1.33.0"),
- ["feature_flag.evaluation.reason"] = ("feature_flag.result.reason", "1.32.0"),
- ["feature_flag.provider_name"] = ("feature_flag.provider.name", "1.33.0"),
- ["feature_flag.variant"] = ("feature_flag.result.variant", "1.32.0"),
- ["http.client_ip"] = ("client.address", "1.21.0"),
- ["http.method"] = ("http.request.method", "1.21.0"),
- ["http.request_content_length_uncompressed"] = ("http.request.body.size", ""),
- ["http.response_content_length_uncompressed"] = ("http.response.body.size", ""),
- ["http.scheme"] = ("url.scheme", "1.21.0"),
- ["http.server_name"] = ("server.address", ""),
- ["http.status_code"] = ("http.response.status_code", "1.21.0"),
- ["http.url"] = ("url.full", "1.21.0"),
- ["http.user_agent"] = ("user_agent.original", "1.19.0"),
- ["ios.state"] = ("ios.app.state", "1.37.0"),
- ["k8s.pod.labels"] = ("k8s.pod.label", "1.25.0"),
- ["linux.memory.slab.state"] = ("system.memory.linux.slab.state", "1.39.0"),
- ["messaging.client_id"] = ("messaging.client.id", "1.26.0"),
- ["messaging.eventhubs.consumer.group"] = ("messaging.consumer.group.name", "1.27.0"),
- ["messaging.kafka.consumer.group"] = ("messaging.consumer.group.name", "1.27.0"),
- ["messaging.kafka.message.offset"] = ("messaging.kafka.offset", "1.27.0"),
- ["messaging.operation"] = ("messaging.operation.type", "1.25.0"),
- ["messaging.servicebus.destination.subscription_name"] = ("messaging.destination.subscription.name", "1.27.0"),
- ["net.host.ip"] = ("network.local.address", "1.13.0"),
- ["net.host.name"] = ("server.address", "1.21.0"),
- ["net.host.port"] = ("server.port", "1.21.0"),
- ["net.peer.ip"] = ("network.peer.address", "1.13.0"),
- ["net.protocol.name"] = ("network.protocol.name", "1.21.0"),
- ["net.protocol.version"] = ("network.protocol.version", "1.21.0"),
- ["net.sock.host.addr"] = ("network.local.address", "1.21.0"),
- ["net.sock.host.port"] = ("network.local.port", "1.21.0"),
- ["net.sock.peer.addr"] = ("network.peer.address", ""),
- ["net.sock.peer.port"] = ("network.peer.port", ""),
- ["net.transport"] = ("network.transport", ""),
- ["otel.library.name"] = ("otel.scope.name", ""),
- ["otel.library.version"] = ("otel.scope.version", ""),
- ["peer.service"] = ("service.peer.name", "1.39.0"),
- ["pool.name"] = ("db.client.connection.pool.name", "1.26.0"),
- ["process.context_switch_type"] = ("process.context_switch.type", "1.38.0"),
- ["process.cpu.state"] = ("cpu.mode", "1.27.0"),
- ["process.executable.build_id.profiling"] = ("process.executable.build_id.htlhash", "1.29.0"),
- ["process.paging.fault_type"] = ("system.paging.fault.type", "1.38.0"),
- ["rpc.connect_rpc.error_code"] = ("rpc.response.status_code", "1.39.0"),
- ["rpc.connect_rpc.request.metadata"] = ("rpc.request.metadata", "1.39.0"),
- ["rpc.connect_rpc.response.metadata"] = ("rpc.response.metadata", "1.39.0"),
- ["rpc.grpc.request.metadata"] = ("rpc.request.metadata", "1.39.0"),
- ["rpc.grpc.response.metadata"] = ("rpc.response.metadata", "1.39.0"),
- ["rpc.jsonrpc.request_id"] = ("jsonrpc.request.id", "1.39.0"),
- ["rpc.jsonrpc.version"] = ("jsonrpc.protocol.version", "1.39.0"),
- ["rpc.system"] = ("rpc.system.name", "1.39.0"),
- ["state"] = ("db.client.connection.state", "1.22.0"),
- ["system.cpu.logical_number"] = ("cpu.logical_number", "1.31.0"),
- ["system.cpu.state"] = ("cpu.mode", "1.27.0"),
- ["system.network.state"] = ("network.connection.state", "1.30.0"),
- ["system.paging.type"] = ("system.paging.fault.type", "1.38.0"),
- ["system.process.status"] = ("process.state", "1.38.0"),
- ["system.processes.status"] = ("process.state", "1.25.0"),
- ["tls.client.server_name"] = ("server.address", "1.27.0"),
- ["vcs.repository.change.id"] = ("vcs.change.id", "1.29.0"),
- ["vcs.repository.change.title"] = ("vcs.change.title", "1.29.0"),
- ["vcs.repository.ref.name"] = ("vcs.ref.head.name", "1.29.0"),
- ["vcs.repository.ref.revision"] = ("vcs.ref.head.revision", "1.29.0"),
- ["vcs.repository.ref.type"] = ("vcs.ref.head.type", "1.29.0"),
- };
-
- private static readonly Dictionary s_deprecatedAttributePrefixes =
- new(StringComparer.OrdinalIgnoreCase) {
- ["db.elasticsearch.path_parts."] = ("db.operation.parameter.", "1.30.0"),
- };
-
- private static readonly Dictionary s_deprecatedGenAiAttributes = new(StringComparer.OrdinalIgnoreCase) {
- ["gen_ai.openai.request.response_format"] = "gen_ai.output.type",
- ["gen_ai.openai.request.seed"] = "gen_ai.request.seed",
- ["gen_ai.openai.request.service_tier"] = "openai.request.service_tier",
- ["gen_ai.openai.response.service_tier"] = "openai.response.service_tier",
- ["gen_ai.openai.response.system_fingerprint"] = "openai.response.system_fingerprint",
- ["gen_ai.system"] = "gen_ai.provider.name",
- ["gen_ai.usage.completion_tokens"] = "gen_ai.usage.output_tokens",
- ["gen_ai.usage.prompt_tokens"] = "gen_ai.usage.input_tokens",
- };
-
- private static readonly Dictionary> s_deprecatedAttributeValues =
- new(StringComparer.OrdinalIgnoreCase) {
- ["cloud.platform"] = new(StringComparer.OrdinalIgnoreCase) {
- ["azure_aks"] = "Use 'azure.aks' instead.",
- ["azure_app_service"] = "Use 'azure.app_service' instead.",
- ["azure_container_apps"] = "Use 'azure.container_apps' instead.",
- ["azure_container_instances"] = "Use 'azure.container_instances' instead.",
- ["azure_functions"] = "Use 'azure.functions' instead.",
- ["azure_openshift"] = "Use 'azure.openshift' instead.",
- ["azure_vm"] = "Use 'azure.vm' instead.",
- },
- ["db.system"] = new(StringComparer.OrdinalIgnoreCase) {
- ["cache"] = "Use 'intersystems_cache' instead.",
- ["cloudscape"] = "Use 'other_sql' instead.",
- ["coldfusion"] = "No replacement exists at this time.",
- ["firstsql"] = "Use 'other_sql' instead.",
- ["mssqlcompact"] = "Use 'other_sql' instead.",
- },
- ["gen_ai.system"] = new(StringComparer.OrdinalIgnoreCase) {
- ["az.ai.inference"] = "Use 'azure.ai.inference' instead.",
- ["az.ai.openai"] = "Use 'azure.ai.openai' instead.",
- ["gemini"] = "Use 'gcp.gemini' instead.",
- ["vertex_ai"] = "Use 'gcp.vertex_ai' instead.",
- },
- ["messaging.operation.type"] = new(StringComparer.OrdinalIgnoreCase) {
- ["deliver"] = "Use 'process' instead.",
- ["publish"] = "Use 'send' instead.",
- },
- ["os.type"] = new(StringComparer.OrdinalIgnoreCase) {
- ["z_os"] = "Use 'zos' instead.",
- },
- ["system.memory.state"] = new(StringComparer.OrdinalIgnoreCase) {
- ["shared"] = "Removed, report shared memory usage with `metric.system.memory.linux.shared` metric",
- },
- ["vcs.provider.name"] = new(StringComparer.OrdinalIgnoreCase) {
- ["gittea"] = "Use 'gitea' instead.",
- },
- };
-
- private static readonly Dictionary s_contextSensitiveDeprecatedNames = new(StringComparer.OrdinalIgnoreCase) {
- ["code.function"] = "Value should be included in `code.function.name` which is expected to be a fully-qualified name.",
- ["code.namespace"] = "Value should be included in `code.function.name` which is expected to be a fully-qualified name.",
- ["db.connection_string"] = "Replaced by `server.address` and `server.port`.",
- ["db.cosmosdb.operation_type"] = "Removed, no replacement at this time.",
- ["db.cosmosdb.status_code"] = "Use `db.response.status_code` instead.",
- ["db.instance.id"] = "Removed, no general replacement at this time. For Elasticsearch, use `db.elasticsearch.node.name` instead.",
- ["db.jdbc.driver_classname"] = "Removed, no replacement at this time.",
- ["db.mssql.instance_name"] = "Removed, no replacement at this time.",
- ["db.redis.database_index"] = "Use `db.namespace` instead.",
- ["db.sql.table"] = "Replaced by `db.collection.name`, but only if not extracting the value from `db.query.text`.",
- ["db.user"] = "Removed, no replacement at this time.",
- ["enduser.role"] = "Use `user.roles` instead.",
- ["enduser.scope"] = "Removed, no replacement at this time.",
- ["error.message"] = "Use domain-specific error message attribute. For example, use `feature_flag.error.message` for feature flag errors.",
- ["event.az.resource.log"] = "Use 'azure.resource.log' instead.",
- ["event.gen_ai.assistant.message"] = "Chat history is reported on `gen_ai.input.messages` attribute on spans or `gen_ai.client.inference.operation.details` event.",
- ["event.gen_ai.choice"] = "Chat history is reported on `gen_ai.output.messages` attribute on spans or `gen_ai.client.inference.operation.details` event.",
- ["event.gen_ai.system.message"] = "Chat history is reported on `gen_ai.system_instructions` attribute on spans or `gen_ai.client.inference.operation.details` event.",
- ["event.gen_ai.tool.message"] = "Chat history is reported on `gen_ai.input.messages` attribute on spans or `gen_ai.client.inference.operation.details` event.",
- ["event.gen_ai.user.message"] = "Chat history is reported on `gen_ai.input.messages` attribute on spans or `gen_ai.client.inference.operation.details` event.",
- ["event.name"] = "The value of this attribute MUST now be set as the value of the EventName field on the LogRecord to indicate that the LogRecord represents an Event.",
- ["event.rpc.message"] = "Deprecated, no replacement at this time.",
- ["exception.escaped"] = "It's no longer recommended to record exceptions that are handled and do not escape the scope of a span.",
- ["gen_ai.completion"] = "Removed, no replacement at this time.",
- ["gen_ai.prompt"] = "Removed, no replacement at this time.",
- ["http.flavor"] = "Split into `network.protocol.name` and `network.protocol.version`",
- ["http.host"] = "Replaced by one of `server.address`, `client.address` or `http.request.header.host`, depending on the usage.",
- ["http.request_content_length"] = "Replaced by `http.request.header.content-length`.",
- ["http.response_content_length"] = "Replaced by `http.response.header.content-length`.",
- ["http.target"] = "Split to `url.path` and `url.query`.",
- ["message.compressed_size"] = "Deprecated, no replacement at this time.",
- ["message.id"] = "Deprecated, no replacement at this time.",
- ["message.type"] = "Deprecated, no replacement at this time.",
- ["message.uncompressed_size"] = "Deprecated, no replacement at this time.",
- ["messaging.destination_publish.anonymous"] = "Removed. No replacement at this time.",
- ["messaging.destination_publish.name"] = "Removed. No replacement at this time.",
- ["messaging.kafka.destination.partition"] = "Record string representation of the partition id in `messaging.destination.partition.id` attribute.",
- ["messaging.rocketmq.client_group"] = "Replaced by `messaging.consumer.group.name` on the consumer spans. No replacement for producer spans.",
- ["net.peer.name"] = "Replaced by `server.address` on client spans and `client.address` on server spans.",
- ["net.peer.port"] = "Replaced by `server.port` on client spans and `client.port` on server spans.",
- ["net.sock.family"] = "Split to `network.transport` and `network.type`.",
- ["net.sock.peer.name"] = "Removed. No replacement at this time.",
- ["rpc.grpc.status_code"] = "Use string representation of the gRPC status code on the `rpc.response.status_code` attribute.",
- ["rpc.jsonrpc.error_code"] = "Use string representation of the error code on the `rpc.response.status_code` attribute.",
- ["rpc.jsonrpc.error_message"] = "Use the span status description when reporting JSON-RPC spans.",
- ["rpc.message.compressed_size"] = "Deprecated, no replacement at this time.",
- ["rpc.message.id"] = "Deprecated, no replacement at this time.",
- ["rpc.message.type"] = "Deprecated, no replacement at this time.",
- ["rpc.message.uncompressed_size"] = "Deprecated, no replacement at this time.",
- ["rpc.service"] = "Value should be included in `rpc.method` which is expected to be a fully-qualified name.",
- };
-
- internal static bool TryGetDeprecatedAttribute(
- string attributeName,
- out (string Replacement, string Version) info) {
- if (s_deprecatedAttributes.TryGetValue(attributeName, out info)) {
- return true;
- }
-
- foreach (var prefix in s_deprecatedAttributePrefixes) {
- if (attributeName.StartsWith(prefix.Key, StringComparison.OrdinalIgnoreCase)) {
- var suffix = attributeName[prefix.Key.Length..];
- info = ($"{prefix.Value.ReplacementPrefix}{suffix}", prefix.Value.Version);
- return true;
- }
- }
-
- info = default;
- return false;
- }
-
- internal static bool TryGetDeprecatedGenAiAttribute(string attributeName, [NotNullWhen(true)] out string? replacement) =>
- s_deprecatedGenAiAttributes.TryGetValue(attributeName, out replacement);
-
- internal static bool TryGetDeprecatedAttributeValue(
- string attributeName,
- string attributeValue,
- [NotNullWhen(true)] out string? guidance) {
- if (s_deprecatedAttributeValues.TryGetValue(attributeName, out var values)
- && values.TryGetValue(attributeValue, out guidance)) {
- return true;
- }
-
- guidance = null;
- return false;
- }
-
- internal static bool TryGetContextSensitiveDeprecatedName(string name, [NotNullWhen(true)] out string? guidance) =>
- s_contextSensitiveDeprecatedNames.TryGetValue(name, out guidance);
-}
diff --git a/src/ANcpLua.Analyzers/Semconv/OpenTelemetryGenAiSemconvFacts.cs b/src/ANcpLua.Analyzers/Semconv/OpenTelemetryGenAiSemconvFacts.cs
deleted file mode 100644
index 508fbcf..0000000
--- a/src/ANcpLua.Analyzers/Semconv/OpenTelemetryGenAiSemconvFacts.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-namespace ANcpLua.Analyzers.Analyzers;
-
-internal static partial class OpenTelemetryGenAiSemconvFacts {
- internal static readonly string[] s_requiredAttributeKeys = [
- "gen_ai.provider.name",
- "gen_ai.request.model",
- "gen_ai.operation.name"
- ];
-
- private static readonly HashSet s_validProviderNames = new(StringComparer.OrdinalIgnoreCase) {
- "openai",
- "anthropic",
- "cohere",
- "azure.ai.inference",
- "azure.ai.openai",
- "gcp.gen_ai",
- "gcp.vertex_ai",
- "gcp.gemini",
- "ibm.watsonx.ai",
- "aws.bedrock",
- "perplexity",
- "x_ai",
- "deepseek",
- "groq",
- "mistral_ai"
- };
-
- private static readonly HashSet s_validOperationNames = new(StringComparer.OrdinalIgnoreCase) {
- "chat",
- "generate_content",
- "text_completion",
- "embeddings",
- "retrieval",
- "create_agent",
- "invoke_agent",
- "execute_tool",
- "invoke_workflow"
- };
-
- internal static bool IsValidProviderName(string value) => s_validProviderNames.Contains(value);
-
- internal static bool IsValidOperationName(string value) => s_validOperationNames.Contains(value);
-}
diff --git a/tests/ANcpLua.Analyzers.Tests/AL0013MissingSchemaUrlTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0013MissingSchemaUrlTests.cs
deleted file mode 100644
index 98dff6b..0000000
--- a/tests/ANcpLua.Analyzers.Tests/AL0013MissingSchemaUrlTests.cs
+++ /dev/null
@@ -1,192 +0,0 @@
-using ANcpLua.Analyzers.Analyzers;
-using ANcpLua.Roslyn.Utilities.Testing;
-
-namespace ANcpLua.Analyzers.Tests;
-
-///
-/// Tests for AL0013: Detects OpenTelemetry configurations that don't set the schema URL.
-///
-///
-/// This analyzer requires OpenTelemetry types to be present.
-/// Tests use stubs to simulate the OpenTelemetry API.
-///
-public sealed partial class Al0013MissingSchemaUrlTests : AnalyzerTest {
- [Theory]
- [InlineData("""
- using System;
- using System.Collections.Generic;
-
- namespace OpenTelemetry.Trace {
- public abstract class TracerProviderBuilder {
- public TracerProviderBuilder ConfigureResource(Action configure) => this;
- public TracerProviderBuilder SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder builder) => this;
- }
- }
-
- namespace OpenTelemetry.Resources {
- public class ResourceBuilder {
- public static ResourceBuilder CreateDefault() => new();
- public ResourceBuilder AddService(string serviceName) => this;
- public ResourceBuilder AddAttributes(IEnumerable> attributes) => this;
- }
- }
-
- public class TestTracerBuilder : OpenTelemetry.Trace.TracerProviderBuilder { }
-
- public class C {
- void M(TestTracerBuilder builder) {
- builder.{|AL0013:ConfigureResource|}(r => r.AddService("MyService"));
- }
- }
- """)]
- [InlineData("""
- using System;
- using System.Collections.Generic;
-
- namespace OpenTelemetry.Trace {
- public abstract class TracerProviderBuilder {
- public TracerProviderBuilder ConfigureResource(Action configure) => this;
- public TracerProviderBuilder SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder builder) => this;
- }
- }
-
- namespace OpenTelemetry.Resources {
- public class ResourceBuilder {
- public static ResourceBuilder CreateDefault() => new();
- public ResourceBuilder AddService(string serviceName) => this;
- public ResourceBuilder AddAttributes(IEnumerable> attributes) => this;
- }
- }
-
- public class TestTracerBuilder : OpenTelemetry.Trace.TracerProviderBuilder { }
-
- public class C {
- void M(TestTracerBuilder builder) {
- builder.{|AL0013:SetResourceBuilder|}(OpenTelemetry.Resources.ResourceBuilder.CreateDefault().AddService("MyService"));
- }
- }
- """)]
- [InlineData("""
- using System;
- using System.Collections.Generic;
-
- namespace OpenTelemetry.Metrics {
- public abstract class MeterProviderBuilder {
- public MeterProviderBuilder ConfigureResource(Action configure) => this;
- }
- }
-
- namespace OpenTelemetry.Resources {
- public class ResourceBuilder {
- public static ResourceBuilder CreateDefault() => new();
- public ResourceBuilder AddService(string serviceName) => this;
- public ResourceBuilder AddAttributes(IEnumerable> attributes) => this;
- }
- }
-
- public class TestMeterBuilder : OpenTelemetry.Metrics.MeterProviderBuilder { }
-
- public class C {
- void M(TestMeterBuilder builder) {
- builder.{|AL0013:ConfigureResource|}(r => r.AddService("MyService"));
- }
- }
- """)]
- [InlineData("""
- using System;
- using System.Collections.Generic;
-
- namespace OpenTelemetry.Logs {
- public abstract class LoggerProviderBuilder {
- public LoggerProviderBuilder ConfigureResource(Action configure) => this;
- }
- }
-
- namespace OpenTelemetry.Resources {
- public class ResourceBuilder {
- public static ResourceBuilder CreateDefault() => new();
- public ResourceBuilder AddService(string serviceName) => this;
- public ResourceBuilder AddAttributes(IEnumerable> attributes) => this;
- }
- }
-
- public class TestLoggerBuilder : OpenTelemetry.Logs.LoggerProviderBuilder { }
-
- public class C {
- void M(TestLoggerBuilder builder) {
- builder.{|AL0013:ConfigureResource|}(r => r.AddService("MyService"));
- }
- }
- """)]
- public Task ShouldReportMissingSchemaUrl(string source) => VerifyAsync(source);
-
- [Theory]
- [InlineData("""
- using System;
- using System.Collections.Generic;
-
- namespace OpenTelemetry.Trace {
- public abstract class TracerProviderBuilder {
- public TracerProviderBuilder ConfigureResource(Action configure) => this;
- }
- }
-
- namespace OpenTelemetry.Resources {
- public class ResourceBuilder {
- public static ResourceBuilder CreateDefault() => new();
- public ResourceBuilder AddService(string serviceName) => this;
- public ResourceBuilder AddAttributes(IEnumerable> attributes) => this;
- }
- }
-
- public class TestTracerBuilder : OpenTelemetry.Trace.TracerProviderBuilder { }
-
- public class C {
- void M(TestTracerBuilder builder) {
- builder.ConfigureResource(r => r.AddAttributes(new[] {
- new KeyValuePair("telemetry.schema_url", "https://opentelemetry.io/schemas/1.21.0")
- }));
- }
- }
- """)]
- [InlineData("""
- using System;
- using System.Collections.Generic;
-
- namespace OpenTelemetry.Trace {
- public abstract class TracerProviderBuilder {
- public TracerProviderBuilder ConfigureResource(Action configure) => this;
- }
- }
-
- namespace OpenTelemetry.Resources {
- public class ResourceBuilder {
- public static ResourceBuilder CreateDefault() => new();
- public ResourceBuilder AddService(string serviceName) => this;
- public ResourceBuilder AddAttributes(IEnumerable> attributes) => this;
- }
- }
-
- public class TestTracerBuilder : OpenTelemetry.Trace.TracerProviderBuilder { }
-
- public class C {
- void M(TestTracerBuilder builder) {
- builder.ConfigureResource(r => r.AddAttributes(new[] {
- new KeyValuePair("schema", "v1")
- }));
- }
- }
- """)]
- public Task ShouldNotReportWhenSchemaUrlPresent(string source) => VerifyAsync(source);
-
- [Fact]
- public Task ShouldNotReportWhenNoOtelTypes() => VerifyAsync(
- """
- public class C {
- void ConfigureResource(System.Action