From b92d6d7b7bb2e7a933da1f16cd21ddf6256769bc Mon Sep 17 00:00:00 2001 From: ancplua Date: Fri, 8 May 2026 02:01:31 +0200 Subject: [PATCH 1/4] remove: 40 telemetry-adjacent rules now homed in ANcpLua.OpenTelemetry.SemanticConventions.Analyzers Pair to ANcpLua/ANcpLua.OpenTelemetry.SemanticConventions.Analyzers PR migrate/import-otel-rules-from-AL. Rules listed below are deleted from this pack and re-imported under the donation-track repo's Imports/ subtree. NuGet consumers picking up the next minor of ANcpLua.Analyzers should reference ANcpLua.OpenTelemetry.SemanticConventions.Analyzers for equivalent rule behaviour. ID renumbering AL00xx -> OTSC00xx happens in the OTSC repo's follow-up PR; this commit only removes the source / test / codefix files from this pack. Removed analyzers (40): OpenTelemetry: AL0013, 0061, 0063, 0073, 0076, 0077, 0078, 0079, 0085, 0086, 0088, 0089, 0090, 0091, 0092, 0093, 0107, 0108, 0109, 0110, 0113, 0135 GenAI: AL0064, 0065, 0066, 0074, 0124, 0128, 0129, 0130, 0131 Metrics: AL0067, 0068, 0071, 0072, 0075 Configuration: AL0069, 0070, 0083, 0096 Removed codefixes (9): for AL0071, 0072, 0073, 0074, 0107, 0108, 0109, 0110, 0124. Removed tests (31): one or two test files per rule (some rules had no standalone test file in this repo). Removed shared semconv helpers: src/.../Semconv/OpenTelemetryDeprecatedSemconvCatalog.cs src/.../Semconv/OpenTelemetryGenAiSemconvFacts.cs (only consumed by the moved OTel rules; the Semconv directory is left empty by this commit and will be pruned by Git automatically.) Resources.resx entries for the removed AL00AnalyzerTitle / MessageFormat / Description keys remain in place for now (dead but harmless). They get pruned in a follow-up alongside CodeFixResources.resx parity cleanup. AnalyzerReleases tracking files are intentionally not edited in this commit per maintainer no-md-writes rule; the removed rules were all Unshipped, so their entries are simply deleted by the next regeneration of the file. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../AL0071MeterClassCodeFixProvider.cs | 86 ---- .../AL0072MetricMethodCodeFixProvider.cs | 70 ---- .../CodeFixes/AL0073TracedCodeFixProvider.cs | 125 ------ .../AL0074DeprecatedGenAiCodeFixProvider.cs | 44 --- .../AL0107OrphanedTracedTagCodeFixProvider.cs | 76 ---- .../AL0108RedundantNoTraceCodeFixProvider.cs | 76 ---- ...09NonInterceptableTracedCodeFixProvider.cs | 76 ---- .../AL0110TracedTagOnOutRefCodeFixProvider.cs | 76 ---- .../AL0124AgentTracedCodeFixProvider.cs | 75 ---- .../AL0013MissingSchemaUrlAnalyzer.cs | 156 -------- .../AL0061ActivityMissingSemconvAnalyzer.cs | 138 ------- ...L0063UnregisteredActivitySourceAnalyzer.cs | 183 --------- ...4GenAiMissingRequiredAttributesAnalyzer.cs | 91 ----- .../AL0065UseTokenUsageHistogramAnalyzer.cs | 67 ---- ...AL0066InvalidGenAiOperationNameAnalyzer.cs | 51 --- .../AL0067UnregisteredMeterAnalyzer.cs | 96 ----- .../AL0068InvalidMetricNameAnalyzer.cs | 71 ---- ...AL0069IncompleteServiceDefaultsAnalyzer.cs | 90 ----- .../AL0070NonOtlpCollectorEndpointAnalyzer.cs | 68 ---- ...71MeterClassMustBePartialStaticAnalyzer.cs | 71 ---- ...AL0072MetricMethodMustBePartialAnalyzer.cs | 86 ---- .../AL0073TracedActivitySourceNameAnalyzer.cs | 85 ---- .../AL0074DeprecatedGenAiAttributeAnalyzer.cs | 137 ------- .../AL0075HighCardinalityMetricTagAnalyzer.cs | 133 ------- .../AL0076MissingOTelConfigurationAnalyzer.cs | 88 ----- .../AL0077DuplicateInstrumentationAnalyzer.cs | 105 ----- ...AL0078InvalidActivitySourceNameAnalyzer.cs | 76 ---- .../AL0079ManualSpanRecommendedAnalyzer.cs | 171 -------- .../AL0083InsecureEndpointAnalyzer.cs | 146 ------- .../AL0085InvalidAttributeValueAnalyzer.cs | 151 ------- .../AL0086IncorrectAttributeTypeAnalyzer.cs | 181 --------- .../AL0088SensitiveDataInAttributeAnalyzer.cs | 212 ---------- .../AL0089MissingOtlpConfigurationAnalyzer.cs | 138 ------- .../AL0090UncompressedExportAnalyzer.cs | 209 ---------- .../AL0091BatchExportDisabledAnalyzer.cs | 58 --- .../AL0092ConsiderSamplingAnalyzer.cs | 172 -------- ...AL0093MissingResourceAttributesAnalyzer.cs | 90 ----- .../AL0096EnableEventSourceSupportAnalyzer.cs | 54 --- .../AL0107OrphanedTracedTagAnalyzer.cs | 89 ----- .../AL0108RedundantNoTraceAnalyzer.cs | 84 ---- .../AL0109NonInterceptableTracedAnalyzer.cs | 73 ---- ...L0110TracedTagOnOutRefParameterAnalyzer.cs | 69 ---- ...ingExceptionRecordingOnActivityAnalyzer.cs | 202 ---------- ...0124NonInterceptableAgentTracedAnalyzer.cs | 60 --- ...tructiveToolMustRequireApprovalAnalyzer.cs | 106 ----- ...AL0129ToolMustDeclareSideEffectAnalyzer.cs | 62 --- ...AL0130ToolMustDeclareCapabilityAnalyzer.cs | 63 --- .../AL0131DirectGenAiSdkUsageAnalyzer.cs | 99 ----- ...gacySemanticConventionsAccessorAnalyzer.cs | 73 ---- .../OpenTelemetryDeprecatedSemconvCatalog.cs | 253 ------------ .../Semconv/OpenTelemetryGenAiSemconvFacts.cs | 43 -- .../AL0013MissingSchemaUrlTests.cs | 192 --------- .../AL0061ActivityMissingSemconvTests.cs | 189 --------- .../AL0063UnregisteredActivitySourceTests.cs | 374 ------------------ ...0064GenAiMissingRequiredAttributesTests.cs | 166 -------- .../AL0066InvalidGenAiOperationNameTests.cs | 43 -- .../AL0067UnregisteredMeterTests.cs | 90 ----- .../AL0074DeprecatedGenAiAttributeTests.cs | 63 --- .../AL0076MissingOTelConfigurationTests.cs | 129 ------ .../AL0077DuplicateInstrumentationTests.cs | 143 ------- .../AL0078InvalidActivitySourceNameTests.cs | 126 ------ .../AL0079ManualSpanRecommendedTests.cs | 233 ----------- .../AL0083InsecureEndpointTests.cs | 191 --------- .../AL0085InvalidAttributeValueTests.cs | 238 ----------- .../AL0085OfficialSemconvAlignmentTests.cs | 48 --- .../AL0086IncorrectAttributeTypeTests.cs | 206 ---------- .../AL0086OfficialSemconvAlignmentTests.cs | 48 --- .../AL0088SensitiveDataInAttributeTests.cs | 229 ----------- .../AL0089MissingOtlpConfigurationTests.cs | 218 ---------- .../AL0090UncompressedExportTests.cs | 189 --------- .../AL0091BatchExportDisabledTests.cs | 101 ----- .../AL0092ConsiderSamplingTests.cs | 144 ------- .../AL0093MissingResourceAttributesTests.cs | 89 ----- .../AL0096EnableEventSourceSupportTests.cs | 28 -- .../AL0107OrphanedTracedTagTests.cs | 93 ----- .../AL0108RedundantNoTraceTests.cs | 83 ---- .../AL0109NonInterceptableTracedTests.cs | 91 ----- .../AL0110TracedTagOnOutRefParameterTests.cs | 102 ----- ...issingExceptionRecordingOnActivityTests.cs | 117 ------ .../AL0124NonInterceptableAgentTracedTests.cs | 121 ------ .../AL0131DirectGenAiSdkUsageTests.cs | 110 ------ ...5LegacySemanticConventionsAccessorTests.cs | 89 ----- 82 files changed, 9637 deletions(-) delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0071MeterClassCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0072MetricMethodCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0073TracedCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0074DeprecatedGenAiCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0107OrphanedTracedTagCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0108RedundantNoTraceCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0109NonInterceptableTracedCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0110TracedTagOnOutRefCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers.CodeFixes/CodeFixes/AL0124AgentTracedCodeFixProvider.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0013MissingSchemaUrlAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0061ActivityMissingSemconvAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0063UnregisteredActivitySourceAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0064GenAiMissingRequiredAttributesAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0065UseTokenUsageHistogramAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0066InvalidGenAiOperationNameAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0067UnregisteredMeterAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0068InvalidMetricNameAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0069IncompleteServiceDefaultsAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0070NonOtlpCollectorEndpointAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0071MeterClassMustBePartialStaticAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0072MetricMethodMustBePartialAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0073TracedActivitySourceNameAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0074DeprecatedGenAiAttributeAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0075HighCardinalityMetricTagAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0076MissingOTelConfigurationAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0077DuplicateInstrumentationAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0078InvalidActivitySourceNameAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0079ManualSpanRecommendedAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0083InsecureEndpointAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0085InvalidAttributeValueAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0086IncorrectAttributeTypeAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0088SensitiveDataInAttributeAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0089MissingOtlpConfigurationAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0090UncompressedExportAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0091BatchExportDisabledAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0092ConsiderSamplingAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0093MissingResourceAttributesAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0096EnableEventSourceSupportAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0107OrphanedTracedTagAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0108RedundantNoTraceAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0109NonInterceptableTracedAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0110TracedTagOnOutRefParameterAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0113MissingExceptionRecordingOnActivityAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0124NonInterceptableAgentTracedAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0131DirectGenAiSdkUsageAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0135LegacySemanticConventionsAccessorAnalyzer.cs delete mode 100644 src/ANcpLua.Analyzers/Semconv/OpenTelemetryDeprecatedSemconvCatalog.cs delete mode 100644 src/ANcpLua.Analyzers/Semconv/OpenTelemetryGenAiSemconvFacts.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0013MissingSchemaUrlTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0061ActivityMissingSemconvTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0063UnregisteredActivitySourceTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0064GenAiMissingRequiredAttributesTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0066InvalidGenAiOperationNameTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0067UnregisteredMeterTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0074DeprecatedGenAiAttributeTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0076MissingOTelConfigurationTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0077DuplicateInstrumentationTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0078InvalidActivitySourceNameTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0079ManualSpanRecommendedTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0083InsecureEndpointTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0085InvalidAttributeValueTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0085OfficialSemconvAlignmentTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0086IncorrectAttributeTypeTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0086OfficialSemconvAlignmentTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0088SensitiveDataInAttributeTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0089MissingOtlpConfigurationTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0090UncompressedExportTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0091BatchExportDisabledTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0092ConsiderSamplingTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0093MissingResourceAttributesTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0096EnableEventSourceSupportTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0107OrphanedTracedTagTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0108RedundantNoTraceTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0109NonInterceptableTracedTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0110TracedTagOnOutRefParameterTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0113MissingExceptionRecordingOnActivityTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0124NonInterceptableAgentTracedTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0131DirectGenAiSdkUsageTests.cs delete mode 100644 tests/ANcpLua.Analyzers.Tests/AL0135LegacySemanticConventionsAccessorTests.cs 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/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/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs deleted file mode 100644 index 1cc08d3..0000000 --- a/src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs +++ /dev/null @@ -1,106 +0,0 @@ - -namespace ANcpLua.Analyzers.Analyzers; - -/// -/// AL0128: Detects Loom tools with destructive side effects that do not require approval. -/// -/// -/// -/// When a Loom tool has a destructive side effect (WritesExternalState, MutatesCode, -/// Deploys, or ClosesIssue), it must also declare [RequiresApproval] to ensure -/// governance gates are in place before execution. -/// -/// -/// The ToolSideEffect enum values considered destructive are: -/// -/// WritesExternalState (2) -/// MutatesCode (3) -/// Deploys (4) -/// ClosesIssue (5) -/// -/// -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed partial class Al0128DestructiveToolMustRequireApprovalAnalyzer : AlAnalyzer { - /// The diagnostic identifier for AL0128. - private const string DiagnosticId = "AL0128"; - - private const string LoomToolAttributeName = "LoomTool"; - private const string ToolSideEffectAttributeName = "ToolSideEffect"; - private const string RequiresApprovalAttributeName = "RequiresApproval"; - - /// Minimum enum value that is considered destructive (WritesExternalState = 2). - private const int DestructiveSideEffectThreshold = 2; - - private static readonly string[] s_destructiveSideEffectNames = [ - "WritesExternalState", - "MutatesCode", - "Deploys", - "ClosesIssue" - ]; - - 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 [LoomTool] for missing [RequiresApproval]. - protected override void RegisterActions(AnalysisContext context) => - context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); - - private static void AnalyzeMethod(SymbolAnalysisContext context) { - var method = (IMethodSymbol)context.Symbol; - - if (!method.HasAttributeByShortName(LoomToolAttributeName)) { - return; - } - - if (HasAttributeOnMethodOrType(method, RequiresApprovalAttributeName)) { - return; - } - - if (GetDestructiveSideEffectName(method) is not { } destructiveSideEffect) { - return; - } - - context.ReportDiagnostic(Diagnostic.Create( - s_rule, - method.Locations.FirstOrDefault() ?? Location.None, - method.Name, - destructiveSideEffect)); - } - - private static string? GetDestructiveSideEffectName(IMethodSymbol method) { - var result = FindDestructiveSideEffectInAttributes(method.GetAttributes()); - - return result ?? FindDestructiveSideEffectInAttributes(method.ContainingType?.GetAttributes()); - } - - private static string? FindDestructiveSideEffectInAttributes(ImmutableArray? attributes) { - if (attributes is not { } attrs) { - return null; - } - - foreach (var attribute in attrs) { - if (attribute.AttributeClass?.Name is not ToolSideEffectAttributeName and - not "ToolSideEffectAttribute") { - continue; - } - - if (attribute.ConstructorArguments is [{ Value: int enumValue and >= DestructiveSideEffectThreshold }, ..]) { - return enumValue < DestructiveSideEffectThreshold + s_destructiveSideEffectNames.Length - ? s_destructiveSideEffectNames[enumValue - DestructiveSideEffectThreshold] - : enumValue.ToString(); - } - } - - return null; - } - - private static bool HasAttributeOnMethodOrType(IMethodSymbol method, string attributeName) => - method.HasAttributeByShortName(attributeName) || - method.ContainingType?.HasAttributeByShortName(attributeName) is true; -} diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs deleted file mode 100644 index 9efd37d..0000000 --- a/src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs +++ /dev/null @@ -1,62 +0,0 @@ - -namespace ANcpLua.Analyzers.Analyzers; - -/// -/// AL0129: Detects Loom tools that do not declare their side effect. -/// -/// -/// -/// Every method annotated with [LoomTool] should also have [ToolSideEffect] on the -/// method itself or on its containing type. This makes the governance surface explicit -/// and enables automated policy enforcement. -/// -/// -/// Example of correct usage: -/// -/// [LoomTool] -/// [ToolSideEffect(ToolSideEffects.ReadsOnly)] -/// public Task<string> ReadFile(string path) { ... } -/// -/// -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed partial class Al0129ToolMustDeclareSideEffectAnalyzer : AlAnalyzer { - /// The diagnostic identifier for AL0129. - private const string DiagnosticId = "AL0129"; - - private const string LoomToolAttributeName = "LoomTool"; - private const string ToolSideEffectAttributeName = "ToolSideEffect"; - - private static readonly DiagnosticDescriptor s_rule = CreateRule( - DiagnosticId, - DiagnosticCategories.GenAI, - DiagnosticSeverities.HiddenByDefault); - - /// Gets the diagnostic descriptors for the supported diagnostics. - public override ImmutableArray SupportedDiagnostics => [s_rule]; - - /// Registers symbol actions to analyze methods with [LoomTool] for missing [ToolSideEffect]. - protected override void RegisterActions(AnalysisContext context) => - context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); - - private static void AnalyzeMethod(SymbolAnalysisContext context) { - var method = (IMethodSymbol)context.Symbol; - - if (!method.HasAttributeByShortName(LoomToolAttributeName)) { - return; - } - - if (HasAttributeOnMethodOrType(method, ToolSideEffectAttributeName)) { - return; - } - - context.ReportDiagnostic(Diagnostic.Create( - s_rule, - method.Locations.FirstOrDefault() ?? Location.None, - method.Name)); - } - - private static bool HasAttributeOnMethodOrType(IMethodSymbol method, string attributeName) => - method.HasAttributeByShortName(attributeName) || - method.ContainingType?.HasAttributeByShortName(attributeName) is true; -} diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs deleted file mode 100644 index e470913..0000000 --- a/src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs +++ /dev/null @@ -1,63 +0,0 @@ - -namespace ANcpLua.Analyzers.Analyzers; - -/// -/// AL0130: Detects Loom tools that do not declare any required capabilities. -/// -/// -/// -/// Every method annotated with [LoomTool] should also have at least one -/// [RequiresCapability] attribute on the method itself or on its containing type. -/// This enables fine-grained access control and ensures that the tool's permission -/// requirements are visible at compile time. -/// -/// -/// Example of correct usage: -/// -/// [LoomTool] -/// [RequiresCapability("file:read")] -/// public Task<string> ReadFile(string path) { ... } -/// -/// -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed partial class Al0130ToolMustDeclareCapabilityAnalyzer : AlAnalyzer { - /// The diagnostic identifier for AL0130. - private const string DiagnosticId = "AL0130"; - - private const string LoomToolAttributeName = "LoomTool"; - private const string RequiresCapabilityAttributeName = "RequiresCapability"; - - private static readonly DiagnosticDescriptor s_rule = CreateRule( - DiagnosticId, - DiagnosticCategories.GenAI, - DiagnosticSeverities.HiddenByDefault); - - /// Gets the diagnostic descriptors for the supported diagnostics. - public override ImmutableArray SupportedDiagnostics => [s_rule]; - - /// Registers symbol actions to analyze methods with [LoomTool] for missing [RequiresCapability]. - protected override void RegisterActions(AnalysisContext context) => - context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); - - private static void AnalyzeMethod(SymbolAnalysisContext context) { - var method = (IMethodSymbol)context.Symbol; - - if (!method.HasAttributeByShortName(LoomToolAttributeName)) { - return; - } - - if (HasAttributeOnMethodOrType(method, RequiresCapabilityAttributeName)) { - return; - } - - context.ReportDiagnostic(Diagnostic.Create( - s_rule, - method.Locations.FirstOrDefault() ?? Location.None, - method.Name)); - } - - private static bool HasAttributeOnMethodOrType(IMethodSymbol method, string attributeName) => - method.HasAttributeByShortName(attributeName) || - method.ContainingType?.HasAttributeByShortName(attributeName) is true; -} 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 configure) { } - void M() { - ConfigureResource(r => { }); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0061ActivityMissingSemconvTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0061ActivityMissingSemconvTests.cs deleted file mode 100644 index b365e29..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0061ActivityMissingSemconvTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0061: Detects Activity/Span creation without semantic convention attributes. -/// -public sealed partial class Al0061ActivityMissingSemconvTests : AnalyzerTest { - private const string ActivityStubs = """ - namespace System.Diagnostics { - public class Activity { - public Activity SetTag(string key, string? value) => this; - public Activity SetTag(string key, int value) => this; - public Activity SetTag(string key, object? value) => this; - } - public class ActivitySource { - public ActivitySource(string name) { } - public Activity? StartActivity(string name) => new Activity(); - } - } - """; - - // ── Should report: no semconv attributes ────────────────────────────── - - [Fact] - public Task ShouldReport_WhenNoSetTagCalls() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = [|Source.StartActivity("gen_ai.chat")|]; - } - } - """); - - [Fact] - public Task ShouldReport_WhenSetTagUsesUnrelatedPrefix() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = [|Source.StartActivity("http.request")|]; - activity?.SetTag("custom.tag", "value"); - } - } - """); - - // ── Should NOT report: semconv attributes present ───────────────────── - - [Fact] - public Task ShouldNotReport_WhenRelevantSemconvPresent() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("http.request"); - activity?.SetTag("http.request.method", "GET"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenDeprecatedNetworkSemconvMapsToHttpConvention() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("http.request"); - activity?.SetTag("net.host.name", "example.com"); - } - } - """); - - [Fact] - // `gen_ai.system` is officially deprecated (→ `gen_ai.provider.name` per semconv 1.40) - // but still clearly GenAI semconv. AL0061 shouldn't double-flag it as "missing semconv" - // when the author is already migrating — AL0074 owns the migration message. - public Task ShouldNotReport_WhenDeprecatedGenAiAliasMapsToCurrentConvention() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - activity?.SetTag("gen_ai.system", "openai"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenSetTagUsesConstField() => - VerifyAsync($$""" - {{ActivityStubs}} - - public static class GenAiAttributes { - public const string ProviderName = "gen_ai.provider.name"; - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - activity?.SetTag(GenAiAttributes.ProviderName, "openai"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenSetTagAfterNullGuard() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - if (activity is null) return; - activity.SetTag("gen_ai.provider.name", "openai"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenSetTagAfterNullGuardWithConstField() => - VerifyAsync($$""" - {{ActivityStubs}} - - public static class GenAiAttributes { - public const string ProviderName = "gen_ai.provider.name"; - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - if (activity is null) return; - activity.SetTag(GenAiAttributes.ProviderName, "openai"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenDbSemconvPresent() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("db.query"); - if (activity is null) return; - activity.SetTag("db.system", "postgresql"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenActivityNameIsNotConstant() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M(string name) { - var activity = Source.StartActivity(name); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0063UnregisteredActivitySourceTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0063UnregisteredActivitySourceTests.cs deleted file mode 100644 index fa71110..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0063UnregisteredActivitySourceTests.cs +++ /dev/null @@ -1,374 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0063: Detects ActivitySource instances not registered with AddSource(). -/// -public sealed partial class Al0063UnregisteredActivitySourceTests : AnalyzerTest { - private const string ActivitySourceAndTracerSetup = """ - namespace System.Diagnostics { - public class ActivitySource { - public ActivitySource(string name) { } - public ActivitySource(string name, string? version) { } - } - } - - namespace OpenTelemetry.Trace { - public class TracerProviderBuilder { - public TracerProviderBuilder AddSource(string name) => this; - } - } - """; - - // ── Should report: no AddSource call ──────────────────────────────── - - [Fact] - public Task ShouldReport_WhenNoAddSourceCall() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"my.service"|]); - } - """); - - [Fact] - public Task ShouldReport_WhenAddSourceUseDifferentName() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("other.service"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"my.service"|]); - } - """); - - [Fact] - public Task ShouldReport_MultipleUnregisteredSources() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource A = - new System.Diagnostics.ActivitySource([|"unregistered.a"|]); - private static readonly System.Diagnostics.ActivitySource B = - new System.Diagnostics.ActivitySource([|"unregistered.b"|]); - } - """); - - // ── Should NOT report: AddSource matches ──────────────────────────── - - [Fact] - public Task ShouldNotReport_WhenAddSourceMatchesExactly() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("my.service"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("my.service"); - } - """); - - [Fact] - public Task ShouldNotReport_WhenAddSourceMatchesConst() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public static class Sources { - public const string Name = "my.service"; - } - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource(Sources.Name); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource(Sources.Name); - } - """); - - [Fact] - public Task ShouldNotReport_WhenAddSourceMatchesCrossFile() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - // Simulates cross-file: AddSource in one class, new ActivitySource in another - public class Startup { - public void ConfigureTracing(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("qyl.gen_ai"); - } - } - - public class Instrumentation { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("qyl.gen_ai"); - } - """); - - // ── Wildcard matching ─────────────────────────────────────────────── - - [Fact] - public Task ShouldNotReport_WhenWildcardPatternMatches() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("OpenAI.*"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("OpenAI.Chat"); - } - """); - - [Fact] - public Task ShouldNotReport_WhenMultipleWildcardsOneMatches() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("Azure.AI.OpenAI.*"); - tracing.AddSource("Anthropic.*"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("Anthropic.Client"); - } - """); - - [Fact] - public Task ShouldReport_WhenWildcardDoesNotMatch() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("OpenAI.*"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"Anthropic.Client"|]); - } - """); - - // ── Edge cases ────────────────────────────────────────────────────── - - [Fact] - public Task ShouldNotReport_WhenSourceNameIsNotConstant() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class C { - private static readonly string Name = "my.service"; - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource(Name); - } - """); - - [Fact] - public Task ShouldNotReport_WhenNotActivitySourceType() => - VerifyAsync(""" - public class ActivitySource { - public ActivitySource(string name) { } - } - - public class C { - private static readonly ActivitySource Source = new ActivitySource("my.service"); - } - """); - - [Fact] - public Task ShouldNotReport_RegisteredWithVersionConstructor() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("my.service"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("my.service", "1.0.0"); - } - """); - - [Fact] - public Task ShouldReport_OnlyUnregisteredWhenMixed() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("registered.source"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Registered = - new System.Diagnostics.ActivitySource("registered.source"); - private static readonly System.Diagnostics.ActivitySource Unregistered = - new System.Diagnostics.ActivitySource([|"unregistered.source"|]); - } - """); - - [Fact] - public Task ShouldReport_WhenBareWildcardPattern() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource(".*"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"my.service"|]); - } - """); - - [Fact] - public Task ShouldNotReport_WhenActivitySourceNameIsNonConstant() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class C { - private static string GetName() => "my.service"; - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource(GetName()); - } - """); - - [Fact] - public Task ShouldReport_WhenForeachOverInstanceField() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - private readonly string[] Sources = new[] { "my.service" }; - - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - foreach (var source in Sources) - tracing.AddSource(source); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"my.service"|]); - } - """); - - [Fact] - public Task ShouldNotReport_DuplicateRegistrations() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - tracing.AddSource("my.service"); - tracing.AddSource("my.service"); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("my.service"); - } - """); - - // ── Foreach-over-array resolution ───────────────────────────────── - - [Fact] - public Task ShouldNotReport_WhenRegisteredViaForeachOverArray() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - private static readonly string[] Sources = new[] { "my.service", "other.service" }; - - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - foreach (var source in Sources) - tracing.AddSource(source); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("my.service"); - } - """); - - [Fact] - public Task ShouldNotReport_WhenRegisteredViaForeachOverCollectionExpression() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public class Config { - private static readonly string[] Sources = ["my.service", "other.service"]; - - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - foreach (var source in Sources) - tracing.AddSource(source); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("my.service"); - } - """); - - [Fact] - public Task ShouldNotReport_WhenForeachArrayIncludesConstReference() => - VerifyAsync($$""" - {{ActivitySourceAndTracerSetup}} - - public static class ActivitySources { - public const string GenAi = "qyl.gen_ai"; - } - - public class Config { - private static readonly string[] Sources = [ActivitySources.GenAi, "OpenAI.*"]; - - public void Setup(OpenTelemetry.Trace.TracerProviderBuilder tracing) { - foreach (var source in Sources) - tracing.AddSource(source); - } - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource(ActivitySources.GenAi); - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0064GenAiMissingRequiredAttributesTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0064GenAiMissingRequiredAttributesTests.cs deleted file mode 100644 index e219859..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0064GenAiMissingRequiredAttributesTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0064: Detects GenAI spans that are missing required semantic convention attributes. -/// -public sealed partial class Al0064GenAiMissingRequiredAttributesTests : AnalyzerTest { - private const string ActivityStubs = """ - namespace System.Diagnostics { - public class Activity { - public Activity SetTag(string key, string? value) => this; - public Activity SetTag(string key, int value) => this; - public Activity SetTag(string key, object? value) => this; - } - public class ActivitySource { - public ActivitySource(string name) { } - public Activity? StartActivity(string name) => new Activity(); - } - } - """; - - // ── Should report: missing required attributes ──────────────────────── - - [Fact] - public Task ShouldReport_WhenAllAttributesMissing() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = {|AL0064:{|AL0064:{|AL0064:Source.StartActivity("gen_ai.chat")|}|}|}; - } - } - """); - - [Fact] - public Task ShouldReport_WhenSomeAttributesMissing() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = {|AL0064:{|AL0064:Source.StartActivity("gen_ai.chat")|}|}; - activity?.SetTag("gen_ai.provider.name", "openai"); - } - } - """); - - // ── Should NOT report: all required attributes present ──────────────── - - [Fact] - public Task ShouldNotReport_WhenAllRequiredAttributesPresent() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - activity?.SetTag("gen_ai.provider.name", "openai"); - activity?.SetTag("gen_ai.request.model", "gpt-4"); - activity?.SetTag("gen_ai.operation.name", "chat"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenAllAttributesPresentViaConstants() => - VerifyAsync($$""" - {{ActivityStubs}} - - public static class GenAiAttributes { - public const string ProviderName = "gen_ai.provider.name"; - public const string RequestModel = "gen_ai.request.model"; - public const string OperationName = "gen_ai.operation.name"; - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - activity?.SetTag(GenAiAttributes.ProviderName, "openai"); - activity?.SetTag(GenAiAttributes.RequestModel, "gpt-4"); - activity?.SetTag(GenAiAttributes.OperationName, "chat"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenSetTagAfterNullGuard() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - if (activity is null) return; - activity.SetTag("gen_ai.provider.name", "openai"); - activity.SetTag("gen_ai.request.model", "gpt-4"); - activity.SetTag("gen_ai.operation.name", "chat"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenSetTagAfterNullGuardWithConstants() => - VerifyAsync($$""" - {{ActivityStubs}} - - public static class GenAiAttributes { - public const string ProviderName = "gen_ai.provider.name"; - public const string RequestModel = "gen_ai.request.model"; - public const string OperationName = "gen_ai.operation.name"; - } - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("gen_ai.chat"); - if (activity is null) return; - activity.SetTag(GenAiAttributes.ProviderName, "openai"); - activity.SetTag(GenAiAttributes.RequestModel, "gpt-4"); - activity.SetTag(GenAiAttributes.OperationName, "chat"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenActivityNameIsNotGenAi() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M() { - var activity = Source.StartActivity("http.request"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenActivityNameIsNotConstant() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = new("test"); - - void M(string name) { - var activity = Source.StartActivity(name); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0066InvalidGenAiOperationNameTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0066InvalidGenAiOperationNameTests.cs deleted file mode 100644 index 38ea54c..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0066InvalidGenAiOperationNameTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -public sealed partial class Al0066InvalidGenAiOperationNameTests : AnalyzerTest { - [Theory] - [InlineData("chat")] - [InlineData("generate_content")] - [InlineData("text_completion")] - [InlineData("embeddings")] - [InlineData("retrieval")] - [InlineData("create_agent")] - [InlineData("invoke_agent")] - [InlineData("execute_tool")] - [InlineData("invoke_workflow")] - public Task ShouldNotReportOfficialOperationNames(string operationName) => - VerifyAsync($$""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("gen_ai.operation.name", "{{operationName}}"); - } - } - """); - - [Fact] - public Task ShouldReportUnknownOperationName() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("gen_ai.operation.name", [|"summarize"|]); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0067UnregisteredMeterTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0067UnregisteredMeterTests.cs deleted file mode 100644 index f062978..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0067UnregisteredMeterTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0067: Detects Meter instances not registered via AddMeter() anywhere in the compilation. -/// Uses constant propagation so const-string references count as registrations. -/// -public sealed partial class Al0067UnregisteredMeterTests : AnalyzerTest { - private const string Polyfill = """ - namespace System.Diagnostics.Metrics { - public sealed class Meter { - public Meter(string name) { } - public Meter(string name, string version) { } - } - } - - public sealed class MeterProviderBuilder { - public MeterProviderBuilder AddMeter(params string[] names) => this; - } - """; - - [Fact] - public Task ShouldReportUnregisteredMeter() => VerifyAsync($$""" - {{Polyfill}} - - public class C { - public static readonly System.Diagnostics.Metrics.Meter M = {|AL0067:new System.Diagnostics.Metrics.Meter("qyl.orphan", "1.0")|}; - } - """); - - [Fact] - public Task ShouldNotReportWhenRegisteredWithLiteral() => VerifyAsync($$""" - {{Polyfill}} - - public class C { - public static readonly System.Diagnostics.Metrics.Meter M = new System.Diagnostics.Metrics.Meter("qyl.agent", "1.0"); - - public static void Register(MeterProviderBuilder b) { - b.AddMeter("qyl.agent"); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenRegisteredViaConstReference() => VerifyAsync($$""" - {{Polyfill}} - - public static class Names { - public const string Agent = "qyl.agent"; - } - - public class C { - public static readonly System.Diagnostics.Metrics.Meter M = new System.Diagnostics.Metrics.Meter(Names.Agent, "1.0"); - - public static void Register(MeterProviderBuilder b) { - b.AddMeter(Names.Agent); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenRegisteredViaParamsWithMultipleNames() => VerifyAsync($$""" - {{Polyfill}} - - public class C { - public static readonly System.Diagnostics.Metrics.Meter Agent = new System.Diagnostics.Metrics.Meter("qyl.agent"); - public static readonly System.Diagnostics.Metrics.Meter GenAi = new System.Diagnostics.Metrics.Meter("qyl.genai"); - - public static void Register(MeterProviderBuilder b) { - b.AddMeter("qyl.agent", "qyl.genai"); - } - } - """); - - [Fact] - public Task ShouldReportMeterWhenOnlyOtherMeterRegistered() => VerifyAsync($$""" - {{Polyfill}} - - public class C { - public static readonly System.Diagnostics.Metrics.Meter Registered = new System.Diagnostics.Metrics.Meter("qyl.agent"); - public static readonly System.Diagnostics.Metrics.Meter Orphan = {|AL0067:new System.Diagnostics.Metrics.Meter("qyl.orphan")|}; - - public static void Register(MeterProviderBuilder b) { - b.AddMeter("qyl.agent"); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0074DeprecatedGenAiAttributeTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0074DeprecatedGenAiAttributeTests.cs deleted file mode 100644 index d6a05f8..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0074DeprecatedGenAiAttributeTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -public sealed partial class Al0074DeprecatedGenAiAttributeTests : AnalyzerTest { - [Theory] - [InlineData(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag([|"gen_ai.system"|], "openai"); - } - } - """)] - [InlineData(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag([|"gen_ai.usage.prompt_tokens"|], 42); - } - } - """)] - [InlineData(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag([|"gen_ai.openai.request.service_tier"|], "default"); - } - } - """)] - public Task ShouldReportDeprecatedGenAiKeysInTelemetryContext(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("gen_ai.provider.name", "openai"); - } - } - """)] - [InlineData(""" - public class C { - void M() { - var key = "gen_ai.system"; - } - } - """)] - public Task ShouldNotReportCurrentOrNonTelemetryStrings(string source) => VerifyAsync(source); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0076MissingOTelConfigurationTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0076MissingOTelConfigurationTests.cs deleted file mode 100644 index cf0c413..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0076MissingOTelConfigurationTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0076: Detects when AddServiceDefaults() is called but AddOpenTelemetry() is missing. -/// -public sealed partial class Al0076MissingOTelConfigurationTests : AnalyzerTest { - [Theory] - [InlineData("AddServiceDefaults")] - [InlineData("AddQylServiceDefaults")] - [InlineData("ConfigureOpenTelemetry")] - public Task ShouldReportWhenOTelMissing(string serviceDefaultsMethod) => VerifyAsync( - $$""" - using Microsoft.Extensions.DependencyInjection; - - namespace Microsoft.Extensions.DependencyInjection { - public interface IServiceCollection { } - public static class ServiceExtensions { - public static IServiceCollection {{serviceDefaultsMethod}}(this IServiceCollection services) => services; - } - } - - public class C { - void M(IServiceCollection services) { - {|AL0076:services.{{serviceDefaultsMethod}}()|}; - } - } - """); - - [Theory] - [InlineData("AddServiceDefaults", "AddOpenTelemetry")] - [InlineData("AddServiceDefaults", "WithTracing")] - [InlineData("AddServiceDefaults", "UseOpenTelemetry")] - [InlineData("AddQylServiceDefaults", "AddOpenTelemetry")] - [InlineData("ConfigureOpenTelemetry", "WithTracing")] - public Task ShouldNotReportWhenOTelPresent(string serviceDefaultsMethod, string otelMethod) => VerifyAsync( - $$""" - using Microsoft.Extensions.DependencyInjection; - - namespace Microsoft.Extensions.DependencyInjection { - public interface IServiceCollection { } - public static class ServiceExtensions { - public static IServiceCollection {{serviceDefaultsMethod}}(this IServiceCollection services) => services; - public static IServiceCollection {{otelMethod}}(this IServiceCollection services) => services; - } - } - - public class C { - void M(IServiceCollection services) { - services.{{serviceDefaultsMethod}}(); - services.{{otelMethod}}(); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenAddOpenTelemetryCalledFirst() => VerifyAsync( - """ - using Microsoft.Extensions.DependencyInjection; - - namespace Microsoft.Extensions.DependencyInjection { - public interface IServiceCollection { } - public static class ServiceExtensions { - public static IServiceCollection AddServiceDefaults(this IServiceCollection services) => services; - public static IServiceCollection AddOpenTelemetry(this IServiceCollection services) => services; - } - } - - public class C { - void M(IServiceCollection services) { - services.AddOpenTelemetry(); - services.AddServiceDefaults(); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenNoServiceDefaults() => VerifyAsync( - """ - using Microsoft.Extensions.DependencyInjection; - - namespace Microsoft.Extensions.DependencyInjection { - public interface IServiceCollection { } - public static class ServiceExtensions { - public static IServiceCollection AddSomething(this IServiceCollection services) => services; - } - } - - public class C { - void M(IServiceCollection services) { - services.AddSomething(); - } - } - """); - - [Fact] - public Task ShouldNotReportForUnrelatedMethod() => VerifyAsync( - """ - public class C { - void AddServiceDefaults() { } - void M() { - AddServiceDefaults(); - } - } - """); - - [Fact] - public Task ShouldReportOnlyOncePerMethod() => VerifyAsync( - """ - using Microsoft.Extensions.DependencyInjection; - - namespace Microsoft.Extensions.DependencyInjection { - public interface IServiceCollection { } - public static class ServiceExtensions { - public static IServiceCollection AddServiceDefaults(this IServiceCollection services) => services; - public static IServiceCollection AddQylServiceDefaults(this IServiceCollection services) => services; - } - } - - public class C { - void M(IServiceCollection services) { - {|AL0076:services.AddServiceDefaults()|}; - {|AL0076:services.AddQylServiceDefaults()|}; - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0077DuplicateInstrumentationTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0077DuplicateInstrumentationTests.cs deleted file mode 100644 index d8e233a..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0077DuplicateInstrumentationTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0077: Duplicate instrumentation - method has both auto and manual tracing. -/// -public sealed partial class Al0077DuplicateInstrumentationTests : AnalyzerTest { - private const string Stubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)] - public class TracedAttribute : System.Attribute { - public TracedAttribute() { } - public TracedAttribute(string activitySourceName) { } - public string ActivitySourceName { get; set; } - } - } - - namespace System.Diagnostics { - public class Activity : System.IDisposable { - public void Dispose() { } - } - - public class ActivitySource { - public ActivitySource(string name) { } - public Activity? StartActivity(string name) => null; - public Activity? StartActivity(string name, ActivityKind kind) => null; - } - - public enum ActivityKind { - Internal, - Server, - Client, - Producer, - Consumer - } - } - """; - - [Fact] - public Task ShouldReportWhenMethodHasTracedAttributeAndStartActivityCall() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - private static readonly System.Diagnostics.ActivitySource Source = new("MyApp"); - - {|AL0077:[Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void ProcessOrder() { - using var activity = Source.StartActivity("ProcessOrder"); - }|} - } - """); - - [Fact] - public Task ShouldReportWhenContainingTypeHasTracedAttributeAndMethodHasStartActivity() => - VerifyAsync($$""" - {{Stubs}} - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public class OrderService { - private static readonly System.Diagnostics.ActivitySource Source = new("MyApp"); - - {|AL0077:public void ProcessOrder() { - using var activity = Source.StartActivity("ProcessOrder"); - }|} - } - """); - - [Fact] - public Task ShouldNotReportWhenOnlyTracedAttribute() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void ProcessOrder() { - // No manual Activity.StartActivity call - } - } - """); - - [Fact] - public Task ShouldNotReportWhenOnlyManualStartActivity() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - private static readonly System.Diagnostics.ActivitySource Source = new("MyApp"); - - public void ProcessOrder() { - using var activity = Source.StartActivity("ProcessOrder"); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenNoInstrumentation() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - public void ProcessOrder() { - // Plain method, no instrumentation - } - } - """); - - [Fact] - public Task ShouldReportWhenStartActivityWithKindParameter() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - private static readonly System.Diagnostics.ActivitySource Source = new("MyApp"); - - {|AL0077:[Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void ProcessOrder() { - using var activity = Source.StartActivity("ProcessOrder", System.Diagnostics.ActivityKind.Server); - }|} - } - """); - - [Fact] - public Task ShouldNotReportWhenDifferentMethodHasStartActivity() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - private static readonly System.Diagnostics.ActivitySource Source = new("MyApp"); - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void ProcessOrder() { - // No StartActivity here - } - - public void OtherMethod() { - using var activity = Source.StartActivity("Other"); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0078InvalidActivitySourceNameTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0078InvalidActivitySourceNameTests.cs deleted file mode 100644 index 1b4e41f..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0078InvalidActivitySourceNameTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0078: Detects ActivitySource names that don't follow reverse-DNS naming convention. -/// -public sealed partial class Al0078InvalidActivitySourceNameTests : AnalyzerTest { - private const string ActivitySourceSetup = """ - namespace System.Diagnostics { - public class ActivitySource { - public ActivitySource(string name) { } - public ActivitySource(string name, string? version) { } - } - } - """; - - [Theory] - [InlineData("MySource")] - [InlineData("source")] - [InlineData("TracingSource")] - public Task ShouldReportSingleWordName(string name) => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"{{name}}"|]); - } - """); - - [Theory] - [InlineData("my source")] - [InlineData("tracing source")] - [InlineData("source name with spaces")] - public Task ShouldReportNameWithSpaces(string name) => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"{{name}}"|]); - } - """); - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" ")] - public Task ShouldReportEmptyOrWhitespaceName(string name) => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"{{name}}"|]); - } - """); - - [Theory] - [InlineData("company..component")] - [InlineData(".company.component")] - [InlineData("company.component.")] - public Task ShouldReportNamesWithEmptySegments(string name) => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"{{name}}"|]); - } - """); - - [Theory] - [InlineData("company.product.component")] - [InlineData("com.example.myapp")] - [InlineData("io.opentelemetry.contrib")] - [InlineData("Company.Product.Component")] - [InlineData("com.example.my-app")] - [InlineData("mycompany.v2.tracing")] - public Task ShouldNotReportValidReverseDnsName(string name) => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource("{{name}}"); - } - """); - - [Fact] - public Task ShouldNotReportWhenNameIsNotConstant() => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly string SourceName = "company.product"; - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource(SourceName); - } - """); - - [Fact] - public Task ShouldReportInConstructorWithVersion() => - VerifyAsync($$""" - {{ActivitySourceSetup}} - - public class C { - private static readonly System.Diagnostics.ActivitySource Source = - new System.Diagnostics.ActivitySource([|"InvalidName"|], "1.0.0"); - } - """); - - [Fact] - public Task ShouldNotReportForNonActivitySourceCreation() => - VerifyAsync(""" - public class MySource { - public MySource(string name) { } - } - - public class C { - private static readonly MySource Source = new MySource("SingleWord"); - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0079ManualSpanRecommendedTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0079ManualSpanRecommendedTests.cs deleted file mode 100644 index c14d89f..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0079ManualSpanRecommendedTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0079: Manual span recommended for complex async flows. -/// -public sealed partial class Al0079ManualSpanRecommendedTests : AnalyzerTest { - private const string TracedAttributeDefinition = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)] - public class TracedAttribute : System.Attribute { - public TracedAttribute(string activitySourceName = "") { } - public string ActivitySourceName { get; set; } = ""; - } - } - """; - - [Fact] - public Task ShouldReportTaskWhenAll() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task {|AL0079:ProcessAsync|}() { - var task1 = Task.Delay(100); - var task2 = Task.Delay(200); - await Task.WhenAll(task1, task2); - } - } - """); - - [Fact] - public Task ShouldReportTaskWhenAny() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - public class MyService { - [Traced("MyApp")] - public async Task {|AL0079:ProcessAsync|}() { - var task1 = Task.Delay(100); - var task2 = Task.Delay(200); - await Task.WhenAny(task1, task2); - } - } - """); - - [Fact] - public Task ShouldReportParallelForEach() => VerifyAsync( - $$""" - using System.Collections.Generic; - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task {|AL0079:ProcessAsync|}(IEnumerable items) { - await Parallel.ForEachAsync(items, async (item, ct) => { - await Task.Delay(100, ct); - }); - } - } - """); - - [Fact] - public Task ShouldReportConfigureAwaitFalse() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task {|AL0079:ProcessAsync|}() { - await Task.Delay(100).ConfigureAwait(false); - } - } - """); - - [Fact] - public Task ShouldReportMultipleAwaits() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task {|AL0079:ProcessAsync|}() { - await Task.Delay(100); - await Task.Delay(200); - await Task.Delay(300); - } - } - """); - - [Fact] - public Task ShouldNotReportSimpleAsyncMethod() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task ProcessAsync() { - await Task.Delay(100); - } - } - """); - - [Fact] - public Task ShouldNotReportTwoAwaits() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task ProcessAsync() { - await Task.Delay(100); - await Task.Delay(200); - } - } - """); - - [Fact] - public Task ShouldNotReportWithoutTracedAttribute() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - public class MyService { - public async Task ProcessAsync() { - var task1 = Task.Delay(100); - var task2 = Task.Delay(200); - await Task.WhenAll(task1, task2); - } - } - """); - - [Fact] - public Task ShouldNotReportSyncMethod() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public void Process() { - // Non-async method - no await patterns to analyze - } - } - """); - - [Fact] - public Task ShouldNotReportConfigureAwaitTrue() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task ProcessAsync() { - await Task.Delay(100).ConfigureAwait(true); - } - } - """); - - [Fact] - public Task ShouldNotCountAwaitsInNestedLambdas() => VerifyAsync( - $$""" - using System; - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - [Traced("MyApp")] - public class MyService { - public async Task ProcessAsync() { - // Only 1 await in outer method - await Task.Delay(100); - - // Awaits in lambdas should not count - Func lambda = async () => { - await Task.Delay(100); - await Task.Delay(200); - await Task.Delay(300); - }; - } - } - """); - - [Fact] - public Task ShouldReportMethodWithTracedAttribute() => VerifyAsync( - $$""" - using System.Threading.Tasks; - using Qyl.Instrumentation.Instrumentation; - - {{TracedAttributeDefinition}} - - public class MyService { - [Traced("MyApp.Orders")] - public async Task {|AL0079:ProcessOrdersAsync|}() { - await Task.Delay(100); - await Task.Delay(200); - await Task.Delay(300); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0083InsecureEndpointTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0083InsecureEndpointTests.cs deleted file mode 100644 index 33a039c..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0083InsecureEndpointTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0083: Insecure endpoint. -/// Warns when HTTP endpoints are used where HTTPS is expected. -/// -public sealed partial class Al0083InsecureEndpointTests : AnalyzerTest { - private const string UriPolyfill = """ - namespace System { - public class Uri { - public Uri(string uriString) { } - } - } - """; - - private const string ConfigurationPolyfill = """ - namespace MyApp { - public class ClientConfig { - public string? Endpoint { get; set; } - public string? ServiceUrl { get; set; } - public string? ApiUrl { get; set; } - } - } - """; - - [Fact] - public Task ShouldReportHttpEndpointInPropertyAssignment() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.Endpoint = [|"http://api.example.com/v1"|]; - } - } - """); - - [Fact] - public Task ShouldReportHttpEndpointInServiceUrl() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.ServiceUrl = [|"http://service.example.com"|]; - } - } - """); - - [Fact] - public Task ShouldReportHttpEndpointInApiUrl() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.ApiUrl = [|"http://api.example.com"|]; - } - } - """); - - [Fact] - public Task ShouldReportHttpInUriConstructor() => - VerifyAsync($$""" - {{UriPolyfill}} - - public class C { - void M() { - var uri = new System.Uri([|"http://external.example.com"|]); - } - } - """); - - [Fact] - public Task ShouldNotReportHttpsEndpoint() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.Endpoint = "https://api.example.com/v1"; - } - } - """); - - [Fact] - public Task ShouldNotReportLocalhostHttp() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.Endpoint = "http://localhost:5000"; - } - } - """); - - [Fact] - public Task ShouldNotReportLoopbackHttp() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.Endpoint = "http://127.0.0.1:5000"; - } - } - """); - - [Fact] - public Task ShouldNotReportIpv6LoopbackHttp() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.Endpoint = "http://[::1]:5000"; - } - } - """); - - [Fact] - public Task ShouldNotReportNonEndpointProperty() => - VerifyAsync(""" - public class C { - public string? Name { get; set; } - void M() { - Name = "http://not-an-endpoint.com"; - } - } - """); - - [Fact] - public Task ShouldReportHttpInUriWithPath() => - VerifyAsync($$""" - {{UriPolyfill}} - - public class C { - void M() { - var uri = new System.Uri([|"http://api.example.com/path/to/resource"|]); - } - } - """); - - [Fact] - public Task ShouldNotReportLocalhostWithPath() => - VerifyAsync($$""" - {{UriPolyfill}} - - public class C { - void M() { - var uri = new System.Uri("http://localhost/api/v1"); - } - } - """); - - [Fact] - public Task ShouldReportHttpInEndpointParameter() => - VerifyAsync(""" - public class C { - void Configure(string endpoint) { } - - void M() { - Configure([|"http://external.example.com"|]); - } - } - """); - - [Fact] - public Task ShouldNotReportNonUrlString() => - VerifyAsync($$""" - {{ConfigurationPolyfill}} - - public class C { - void M() { - var config = new MyApp.ClientConfig(); - config.Endpoint = "not-a-url"; - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0085InvalidAttributeValueTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0085InvalidAttributeValueTests.cs deleted file mode 100644 index bb7db7c..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0085InvalidAttributeValueTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0085: Detects attribute values that violate OTel semantic convention specifications. -/// -public sealed partial class Al0085InvalidAttributeValueTests : AnalyzerTest { - /// - /// Polyfill Activity class with typed SetTag overloads to preserve type information. - /// The real System.Diagnostics.Activity.SetTag uses object? which loses type info. - /// - private const string ActivityPolyfill = """ - namespace System.Diagnostics { - public class Activity { - public Activity SetTag(string key, string? value) => this; - public Activity SetTag(string key, int value) => this; - public Activity SetTag(string key, long value) => this; - public Activity SetTag(string key, double value) => this; - public Activity SetTag(string key, bool value) => this; - } - } - """; - - [Theory] - [InlineData("\"not-a-number\"")] - [InlineData("99")] - [InlineData("600")] - public Task ShouldReportInvalidHttpStatusCode(string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("http.response.status_code", [|{{value}}|]); - } - } - """); - - [Theory] - [InlineData("unknown-provider")] - [InlineData("gpt4")] - // OTel semconv 1.40+ moved the provider attribute from `gen_ai.system` to `gen_ai.provider.name`. - public Task ShouldReportInvalidGenAiSystem(string invalidValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("gen_ai.provider.name", [|"{{invalidValue}}"|]); - } - } - """); - - [Theory] - [InlineData("query")] - [InlineData("generate")] - public Task ShouldReportInvalidGenAiOperationName(string invalidValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("gen_ai.operation.name", [|"{{invalidValue}}"|]); - } - } - """); - - [Theory] - [InlineData("INVALID")] - [InlineData("get")] - public Task ShouldReportInvalidHttpMethod(string invalidValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("http.request.method", [|"{{invalidValue}}"|]); - } - } - """); - - [Theory] - [InlineData("gen_ai.usage.input_tokens", "-1")] - [InlineData("gen_ai.usage.output_tokens", "-100")] - public Task ShouldReportNegativeTokenCounts(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", [|{{value}}|]); - } - } - """); - - [Theory] - [InlineData("200")] - [InlineData("404")] - [InlineData("500")] - public Task ShouldNotReportValidHttpStatusCode(string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("http.response.status_code", {{value}}); - } - } - """); - - [Theory] - [InlineData("openai")] - [InlineData("anthropic")] - [InlineData("vertex_ai")] - public Task ShouldNotReportValidGenAiSystem(string validValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("gen_ai.system", "{{validValue}}"); - } - } - """); - - [Theory] - [InlineData("chat")] - [InlineData("embeddings")] - [InlineData("text_completion")] - public Task ShouldNotReportValidGenAiOperationName(string validValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("gen_ai.operation.name", "{{validValue}}"); - } - } - """); - - [Theory] - [InlineData("GET")] - [InlineData("POST")] - [InlineData("_OTHER")] - public Task ShouldNotReportValidHttpMethod(string validValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("http.request.method", "{{validValue}}"); - } - } - """); - - [Theory] - [InlineData("gen_ai.usage.input_tokens", "0")] - [InlineData("gen_ai.usage.output_tokens", "100")] - public Task ShouldNotReportValidTokenCounts(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", {{value}}); - } - } - """); - - [Theory] - [InlineData("custom.attribute", "any-value")] - [InlineData("my.app.operation", "custom-operation")] - public Task ShouldNotReportUnknownAttributes(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", "{{value}}"); - } - } - """); - - [Fact] - public Task ShouldNotReportForNonConstantValue() => VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity, int statusCode) { - activity.SetTag("http.response.status_code", statusCode); - } - } - """); - - [Fact] - // OTel semconv 1.40+ moved the gRPC status code into the unified `rpc.response.status_code` - // attribute. The replacement is typed as string (e.g. "DEADLINE_EXCEEDED"); AL0085 flags - // empty / whitespace-only values. - public Task ShouldReportInvalidRpcResponseStatusCode() => VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("rpc.response.status_code", [|""|]); - } - } - """); - - [Theory] - [InlineData("invalid")] - [InlineData("file")] - public Task ShouldReportInvalidUrlScheme(string invalidValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("url.scheme", [|"{{invalidValue}}"|]); - } - } - """); - - [Theory] - [InlineData("http")] - [InlineData("https")] - public Task ShouldNotReportValidUrlScheme(string validValue) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("url.scheme", "{{validValue}}"); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0085OfficialSemconvAlignmentTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0085OfficialSemconvAlignmentTests.cs deleted file mode 100644 index 77a3705..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0085OfficialSemconvAlignmentTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -public sealed partial class Al0085OfficialSemconvAlignmentTests : AnalyzerTest { - [Fact] - public Task ShouldReportDeprecatedProviderValueForCurrentProviderKey() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("gen_ai.provider.name", [|"vertex_ai"|]); - } - } - """); - - [Fact] - public Task ShouldNotReportCurrentProviderValue() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("gen_ai.provider.name", "gcp.vertex_ai"); - } - } - """); - - [Fact] - public Task ShouldNotReportNewOfficialOperationName() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("gen_ai.operation.name", "invoke_workflow"); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0086IncorrectAttributeTypeTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0086IncorrectAttributeTypeTests.cs deleted file mode 100644 index 563738c..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0086IncorrectAttributeTypeTests.cs +++ /dev/null @@ -1,206 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0086: Detects OpenTelemetry attributes set with incorrect types. -/// -public sealed partial class Al0086IncorrectAttributeTypeTests : AnalyzerTest { - /// - /// Polyfill Activity class with typed SetTag overloads to preserve type information. - /// The real System.Diagnostics.Activity.SetTag uses object? which loses type info. - /// - private const string ActivityPolyfill = """ - namespace System.Diagnostics { - public class Activity { - public Activity SetTag(string key, string? value) => this; - public Activity SetTag(string key, int value) => this; - public Activity SetTag(string key, long value) => this; - public Activity SetTag(string key, double value) => this; - public Activity SetTag(string key, float value) => this; - public Activity SetTag(string key, bool value) => this; - public Activity SetTag(string key, string[]? value) => this; - } - } - """; - - [Theory] - [InlineData("gen_ai.usage.input_tokens", "\"100\"")] - [InlineData("gen_ai.usage.output_tokens", "\"50\"")] - [InlineData("http.response.status_code", "\"200\"")] - [InlineData("db.operation.batch.size", "\"10\"")] - [InlineData("server.port", "\"8080\"")] - public Task ShouldReportStringInsteadOfInt(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", [|{{value}}|]); - } - } - """); - - [Theory] - [InlineData("gen_ai.request.temperature", "\"0.7\"")] - [InlineData("gen_ai.request.top_p", "\"0.9\"")] - public Task ShouldReportStringInsteadOfDouble(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", [|{{value}}|]); - } - } - """); - - [Theory] - [InlineData("gen_ai.usage.input_tokens", "100")] - [InlineData("gen_ai.usage.output_tokens", "50")] - [InlineData("http.response.status_code", "200")] - [InlineData("db.operation.batch.size", "10")] - [InlineData("server.port", "8080")] - public Task ShouldNotReportCorrectIntType(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", {{value}}); - } - } - """); - - [Theory] - [InlineData("gen_ai.request.temperature", "0.7")] - [InlineData("gen_ai.request.temperature", "0.7f")] - [InlineData("gen_ai.request.top_p", "0.9")] - [InlineData("gen_ai.request.top_p", "1")] - public Task ShouldNotReportCorrectDoubleType(string attributeName, string value) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", {{value}}); - } - } - """); - - [Theory] - [InlineData("gen_ai.system")] - [InlineData("gen_ai.request.model")] - [InlineData("http.request.method")] - [InlineData("custom.attribute")] - public Task ShouldNotReportUnknownAttributes(string attributeName) => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", "some_value"); - } - } - """); - - [Fact] - public Task ShouldReportSetAttributeMethod() => - VerifyAsync(""" - public class C { - void SetAttribute(string key, string value) { } - void M() { - SetAttribute("gen_ai.usage.input_tokens", [|"100"|]); - } - } - """); - - [Fact] - public Task ShouldReportAddTagMethod() => - VerifyAsync(""" - public class C { - void AddTag(string key, string value) { } - void M() { - AddTag("http.response.status_code", [|"404"|]); - } - } - """); - - [Fact] - public Task ShouldNotReportNonConstantAttributeName() => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity, string attrName) { - activity.SetTag(attrName, "100"); - } - } - """); - - [Fact] - public Task ShouldAcceptLongForIntAttribute() => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - long tokens = 100L; - activity.SetTag("gen_ai.usage.input_tokens", tokens); - } - } - """); - - [Fact] - public Task ShouldAcceptIntForDoubleAttribute() => - VerifyAsync($$""" - {{ActivityPolyfill}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("gen_ai.request.temperature", 1); - } - } - """); - - private const string ObjectOnlyActivity = """ - namespace System.Diagnostics { - public class Activity { - public Activity SetTag(string key, object? value) => this; - } - } - """; - - [Theory] - [InlineData("gen_ai.usage.input_tokens", "int tokens = 100; activity.SetTag(\"gen_ai.usage.input_tokens\", tokens)")] - [InlineData("gen_ai.usage.output_tokens", "activity.SetTag(\"gen_ai.usage.output_tokens\", 42)")] - [InlineData("gen_ai.request.temperature", "activity.SetTag(\"gen_ai.request.temperature\", 0.7)")] - [InlineData("gen_ai.request.temperature", "activity.SetTag(\"gen_ai.request.temperature\", (double)0.7f)")] - [InlineData("gen_ai.request.max_tokens", "activity.SetTag(\"gen_ai.request.max_tokens\", (int)100L)")] - [InlineData("gen_ai.response.finish_reasons", "activity.SetTag(\"gen_ai.response.finish_reasons\", new string[] { \"stop\" })")] - public Task ShouldNotReportCorrectTypeBoxedToObject(string _, string statement) => - VerifyAsync($$""" - {{ObjectOnlyActivity}} - - public class C { - void M(System.Diagnostics.Activity activity) { - {{statement}}; - } - } - """); - - [Theory] - [InlineData("gen_ai.usage.input_tokens", "\"100\"")] - [InlineData("gen_ai.request.temperature", "\"0.7\"")] - public Task ShouldReportIncorrectTypeBoxedToObject(string attributeName, string value) => - VerifyAsync($$""" - {{ObjectOnlyActivity}} - - public class C { - void M(System.Diagnostics.Activity activity) { - activity.SetTag("{{attributeName}}", [|{{value}}|]); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0086OfficialSemconvAlignmentTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0086OfficialSemconvAlignmentTests.cs deleted file mode 100644 index 596d4b0..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0086OfficialSemconvAlignmentTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -public sealed partial class Al0086OfficialSemconvAlignmentTests : AnalyzerTest { - [Fact] - public Task ShouldReportBatchSizeWhenUsingStringInsteadOfInt() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("db.operation.batch.size", [|"10"|]); - } - } - """); - - [Fact] - public Task ShouldNotReportBatchSizeWhenUsingInt() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("db.operation.batch.size", 10); - } - } - """); - - [Fact] - public Task ShouldReportRpcResponseStatusCodeWhenUsingIntInsteadOfString() => - VerifyAsync(""" - public class Activity { - public void SetTag(string key, object? value) { } - } - - public class C { - void M(Activity activity) { - activity.SetTag("rpc.response.status_code", [|14|]); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0088SensitiveDataInAttributeTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0088SensitiveDataInAttributeTests.cs deleted file mode 100644 index d71ea3d..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0088SensitiveDataInAttributeTests.cs +++ /dev/null @@ -1,229 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0088: Detects potential PII or credential data in span attributes. -/// -public sealed partial class Al0088SensitiveDataInAttributeTests : AnalyzerTest { - [Theory] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attributes = new Dictionary(); - attributes[[|"password"|]] = "secret123"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var tags = new Dictionary(); - tags[[|"user.password"|]] = "secret"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attrs = new Dictionary(); - attrs[[|"api_key"|]] = "sk-12345"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attr = new Dictionary(); - attr[[|"access_token"|]] = "eyJhbGc..."; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attributes = new Dictionary(); - attributes[[|"secret_key"|]] = "abc123"; - } - } - """)] - public Task ShouldReportSensitiveAttributeNamesInDictionary(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attributes = new Dictionary(); - attributes[[|"ssn"|]] = "123-45-6789"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var tags = new Dictionary(); - tags[[|"credit_card"|]] = "4111111111111111"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attrs = new Dictionary(); - attrs[[|"social_security"|]] = "123456789"; - } - } - """)] - public Task ShouldReportPiiAttributeNames(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - public class C { - void SetTag(string key, object value) { } - void M() { - SetTag([|"auth_token"|], "bearer xyz"); - } - } - """)] - [InlineData(""" - public class C { - void AddAttribute(string key, object value) { } - void M() { - AddAttribute([|"bearer"|], "token123"); - } - } - """)] - [InlineData(""" - public class C { - void SetAttribute(string key, object value) { } - void M() { - SetAttribute([|"private_key"|], "-----BEGIN RSA PRIVATE KEY-----"); - } - } - """)] - public Task ShouldReportSensitiveAttributesInTelemetryMethods(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attributes = new Dictionary(); - attributes["http.method"] = "GET"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var tags = new Dictionary(); - tags["user.name"] = "john"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attrs = new Dictionary(); - attrs["db.operation"] = "SELECT"; - } - } - """)] - public Task ShouldNotReportNonSensitiveAttributes(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - public class C { - void M() { - var password = "secret123"; - } - } - """)] - [InlineData(""" - public class C { - void Log(string message) { } - void M() { - Log("user entered password"); - } - } - """)] - public Task ShouldNotReportOutsideTelemetryContext(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var span = new Dictionary(); - span[[|"connection_string"|]] = "Server=localhost;Password=secret"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var activity = new Dictionary(); - activity[[|"credentials"|]] = "user:pass"; - } - } - """)] - public Task ShouldReportInSpanAndActivityContexts(string source) => VerifyAsync(source); - - [Theory] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attributes = new Dictionary(); - attributes[[|"user_authorization"|]] = "Basic xyz"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var tags = new Dictionary(); - tags[[|"cvv"|]] = "123"; - } - } - """)] - [InlineData(""" - using System.Collections.Generic; - - public class C { - void M() { - var attrs = new Dictionary(); - attrs[[|"pin"|]] = "1234"; - } - } - """)] - public Task ShouldReportVariousSensitivePatterns(string source) => VerifyAsync(source); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0089MissingOtlpConfigurationTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0089MissingOtlpConfigurationTests.cs deleted file mode 100644 index 4dfc1a8..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0089MissingOtlpConfigurationTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0089: Detects OTLP exporter calls without explicit endpoint configuration. -/// -public sealed partial class Al0089MissingOtlpConfigurationTests : AnalyzerTest { - [Theory] - [InlineData("UseOtlpExporter")] - [InlineData("AddOtlpExporter")] - public Task ShouldReportWhenEndpointNotConfigured(string methodName) => VerifyAsync( - $$""" - using OpenTelemetry; - - namespace OpenTelemetry { - public static class OTelExtensions { - public static void {{methodName}}(this object builder) { } - } - } - - public class C { - void M() { - var builder = new object(); - {|AL0089:builder.{{methodName}}()|}; - } - } - """); - - [Theory] - [InlineData("UseOtlpExporter")] - [InlineData("AddOtlpExporter")] - public Task ShouldNotReportWhenEndpointSetInLambda(string methodName) => VerifyAsync( - $$""" - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public class OtlpExporterOptions { - public Uri? Endpoint { get; set; } - } - public static class OTelExtensions { - public static void {{methodName}}(this object builder, Action configure) { } - } - } - - public class C { - void M() { - var builder = new object(); - builder.{{methodName}}(options => options.Endpoint = new Uri("http://localhost:4317")); - } - } - """); - - [Theory] - [InlineData("UseOtlpExporter")] - [InlineData("AddOtlpExporter")] - public Task ShouldNotReportWhenEndpointSetInParenthesizedLambda(string methodName) => VerifyAsync( - $$""" - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public class OtlpExporterOptions { - public Uri? Endpoint { get; set; } - } - public static class OTelExtensions { - public static void {{methodName}}(this object builder, Action configure) { } - } - } - - public class C { - void M() { - var builder = new object(); - builder.{{methodName}}((options) => { options.Endpoint = new Uri("http://localhost:4317"); }); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenUriPassedAsSecondArgument() => VerifyAsync( - """ - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public enum OtlpExportProtocol { Grpc, HttpProtobuf } - public static class OTelExtensions { - public static void UseOtlpExporter(this object builder, OtlpExportProtocol protocol, Uri endpoint) { } - } - } - - public class C { - void M() { - var builder = new object(); - builder.UseOtlpExporter(OtlpExportProtocol.Grpc, new Uri("http://collector:4317")); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenNamedEndpointArgumentProvided() => VerifyAsync( - """ - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public static class OTelExtensions { - public static void AddOtlpExporter(this object builder, Uri? endpoint = null) { } - } - } - - public class C { - void M() { - var builder = new object(); - builder.AddOtlpExporter(endpoint: new Uri("http://localhost:4317")); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenEnvironmentVariableSetBefore() => VerifyAsync( - """ - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public static class OTelExtensions { - public static void UseOtlpExporter(this object builder) { } - } - } - - public class C { - void M() { - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317"); - var builder = new object(); - builder.UseOtlpExporter(); - } - } - """); - - [Fact] - public Task ShouldReportWhenEnvironmentVariableSetAfter() => VerifyAsync( - """ - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public static class OTelExtensions { - public static void UseOtlpExporter(this object builder) { } - } - } - - public class C { - void M() { - var builder = new object(); - {|AL0089:builder.UseOtlpExporter()|}; - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317"); - } - } - """); - - [Fact] - public Task ShouldNotReportForUnrelatedMethods() => VerifyAsync( - """ - public class C { - void UseOtlpExporter() { } - void M() { - UseOtlpExporter(); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenOptionsObjectCreationHasEndpoint() => VerifyAsync( - """ - using System; - using OpenTelemetry; - - namespace OpenTelemetry { - public class OtlpExporterOptions { - public Uri? Endpoint { get; set; } - } - public static class OTelExtensions { - public static void AddOtlpExporter(this object builder, OtlpExporterOptions options) { } - } - } - - public class C { - void M() { - var builder = new object(); - builder.AddOtlpExporter(new OtlpExporterOptions { Endpoint = new Uri("http://localhost:4317") }); - } - } - """); - - [Fact] - public Task ShouldReportEachUnconfiguredExporter() => VerifyAsync( - """ - using OpenTelemetry; - - namespace OpenTelemetry { - public static class OTelExtensions { - public static void UseOtlpExporter(this object builder) { } - public static void AddOtlpExporter(this object builder) { } - } - } - - public class C { - void M() { - var builder = new object(); - {|AL0089:builder.UseOtlpExporter()|}; - {|AL0089:builder.AddOtlpExporter()|}; - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0090UncompressedExportTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0090UncompressedExportTests.cs deleted file mode 100644 index fbdfe47..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0090UncompressedExportTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0090: Detects OTLP exporter configurations using HTTP protocol without compression. -/// -/// -/// This analyzer requires OpenTelemetry OTLP exporter types to be present. -/// Tests use stubs to simulate the OpenTelemetry.Exporter API. -/// -public sealed partial class Al0090UncompressedExportTests : AnalyzerTest { - private const string OtlpStubs = """ - namespace OpenTelemetry.Exporter { - public enum OtlpExportProtocol { - Grpc = 0, - HttpProtobuf = 1 - } - - public class OtlpExporterOptions { - public OtlpExportProtocol Protocol { get; set; } - public string? Endpoint { get; set; } - public bool GzipCompression { get; set; } - } - - public class OtlpExporterOptionsBase { - public OtlpExportProtocol Protocol { get; set; } - } - } - - namespace OpenTelemetry.Trace { - public abstract class TracerProviderBuilder { - public TracerProviderBuilder AddOtlpExporter(System.Action? configure = null) => this; - public TracerProviderBuilder UseOtlpExporter(System.Action? configure = null) => this; - } - } - """; - - [Theory] - [InlineData(""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {STUBS} - - public class TestTracerBuilder : TracerProviderBuilder { } - - public class C { - void M(TestTracerBuilder builder) { - builder.{|AL0090:AddOtlpExporter|}(options => { - options.Protocol = OtlpExportProtocol.HttpProtobuf; - }); - } - } - """)] - [InlineData(""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {STUBS} - - public class TestTracerBuilder : TracerProviderBuilder { } - - public class C { - void M(TestTracerBuilder builder) { - builder.{|AL0090:UseOtlpExporter|}(options => { - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.Endpoint = "http://localhost:4318"; - }); - } - } - """)] - public Task ShouldReportHttpProtobufWithoutCompression(string source) => - VerifyAsync(source.Replace("{STUBS}", OtlpStubs, StringComparison.Ordinal)); - - [Theory] - [InlineData(""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {STUBS} - - public class TestTracerBuilder : TracerProviderBuilder { } - - public class C { - void M(TestTracerBuilder builder) { - builder.AddOtlpExporter(options => { - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.GzipCompression = true; - }); - } - } - """)] - [InlineData(""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {STUBS} - - public class TestTracerBuilder : TracerProviderBuilder { } - - public class C { - void M(TestTracerBuilder builder) { - builder.AddOtlpExporter(options => { - options.Protocol = OtlpExportProtocol.Grpc; - }); - } - } - """)] - [InlineData(""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {STUBS} - - public class TestTracerBuilder : TracerProviderBuilder { } - - public class C { - void M(TestTracerBuilder builder) { - // Default (Grpc) - no Protocol explicitly set - builder.AddOtlpExporter(); - } - } - """)] - public Task ShouldNotReportWhenCompressionEnabledOrGrpc(string source) => - VerifyAsync(source.Replace("{STUBS}", OtlpStubs, StringComparison.Ordinal)); - - [Fact] - public Task ShouldNotReportWhenNoOtlpTypes() => VerifyAsync( - """ - public class C { - void AddOtlpExporter(System.Action configure) { } - void M() { - AddOtlpExporter(options => { }); - } - } - """); - - [Fact] - public Task ShouldReportObjectInitializerWithHttpProtobuf() => VerifyAsync( - $$""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {{OtlpStubs}} - - public class TestTracerBuilder : TracerProviderBuilder { - public TracerProviderBuilder AddOtlpExporter(OtlpExporterOptions options) => this; - } - - public class C { - void M(TestTracerBuilder builder) { - builder.{|AL0090:AddOtlpExporter|}(new OtlpExporterOptions { - Protocol = OtlpExportProtocol.HttpProtobuf - }); - } - } - """); - - [Fact] - public Task ShouldNotReportObjectInitializerWithCompression() => VerifyAsync( - $$""" - using System; - using OpenTelemetry.Exporter; - using OpenTelemetry.Trace; - - {{OtlpStubs}} - - public class TestTracerBuilder : TracerProviderBuilder { - public TracerProviderBuilder AddOtlpExporter(OtlpExporterOptions options) => this; - } - - public class C { - void M(TestTracerBuilder builder) { - builder.AddOtlpExporter(new OtlpExporterOptions { - Protocol = OtlpExportProtocol.HttpProtobuf, - GzipCompression = true - }); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0091BatchExportDisabledTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0091BatchExportDisabledTests.cs deleted file mode 100644 index 7c9663c..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0091BatchExportDisabledTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0091: Detects when SimpleSpanProcessor or SimpleActivityExportProcessor is used -/// instead of batch processors. -/// -public sealed partial class Al0091BatchExportDisabledTests : AnalyzerTest { - [Theory] - [InlineData("SimpleSpanProcessor")] - [InlineData("SimpleActivityExportProcessor")] - public Task ShouldReportSimpleProcessor(string processorType) => VerifyAsync( - $$""" - namespace OpenTelemetry.Trace { - public class BaseExporter { } - public class {{processorType}} { - public {{processorType}}(BaseExporter exporter) { } - } - } - - public class C { - void M() { - var exporter = new OpenTelemetry.Trace.BaseExporter(); - var processor = {|AL0091:new OpenTelemetry.Trace.{{processorType}}(exporter)|}; - } - } - """); - - [Theory] - [InlineData("BatchSpanProcessor")] - [InlineData("BatchActivityExportProcessor")] - public Task ShouldNotReportBatchProcessor(string processorType) => VerifyAsync( - $$""" - namespace OpenTelemetry.Trace { - public class BaseExporter { } - public class {{processorType}} { - public {{processorType}}(BaseExporter exporter) { } - } - } - - public class C { - void M() { - var exporter = new OpenTelemetry.Trace.BaseExporter(); - var processor = new OpenTelemetry.Trace.{{processorType}}(exporter); - } - } - """); - - [Fact] - public Task ShouldReportSimpleProcessorWithoutNamespace() => VerifyAsync( - """ - public class BaseExporter { } - public class SimpleSpanProcessor { - public SimpleSpanProcessor(BaseExporter exporter) { } - } - - public class C { - void M() { - var exporter = new BaseExporter(); - var processor = {|AL0091:new SimpleSpanProcessor(exporter)|}; - } - } - """); - - [Fact] - public Task ShouldNotReportUnrelatedTypes() => VerifyAsync( - """ - public class SomeOtherProcessor { - public SomeOtherProcessor() { } - } - - public class C { - void M() { - var processor = new SomeOtherProcessor(); - } - } - """); - - [Fact] - public Task ShouldReportInTracerProviderBuilder() => VerifyAsync( - """ - namespace OpenTelemetry.Trace { - public class BaseExporter { } - public class SimpleSpanProcessor { - public SimpleSpanProcessor(BaseExporter exporter) { } - } - public class TracerProviderBuilder { - public TracerProviderBuilder AddProcessor(SimpleSpanProcessor processor) => this; - } - } - - public class C { - void M(OpenTelemetry.Trace.TracerProviderBuilder builder) { - var exporter = new OpenTelemetry.Trace.BaseExporter(); - builder.AddProcessor({|AL0091:new OpenTelemetry.Trace.SimpleSpanProcessor(exporter)|}); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0092ConsiderSamplingTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0092ConsiderSamplingTests.cs deleted file mode 100644 index bbf02dd..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0092ConsiderSamplingTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0092: Detects OpenTelemetry tracing configurations without sampling configured. -/// -/// -/// This analyzer requires OpenTelemetry types to be present. -/// Tests use stubs to simulate the OpenTelemetry API. -/// -public sealed partial class Al0092ConsiderSamplingTests : AnalyzerTest { - private const string OtelStubs = """ - namespace OpenTelemetry.Trace { - public abstract class TracerProviderBuilder { - public TracerProviderBuilder WithTracing(System.Action configure) => this; - public TracerProviderBuilder SetSampler(Sampler sampler) => this; - public TracerProviderBuilder AddSource(string name) => this; - } - - public abstract class Sampler { } - - public class AlwaysOnSampler : Sampler { } - - public class AlwaysOffSampler : Sampler { } - - public class ParentBasedSampler : Sampler { - public ParentBasedSampler(Sampler rootSampler) { } - } - - public class TraceIdRatioBasedSampler : Sampler { - public TraceIdRatioBasedSampler(double ratio) { } - } - } - - namespace OpenTelemetry { - public interface IOpenTelemetryBuilder { - IOpenTelemetryBuilder WithTracing(System.Action configure); - } - } - - public class TestTracerBuilder : OpenTelemetry.Trace.TracerProviderBuilder { } - - public class TestOtelBuilder : OpenTelemetry.IOpenTelemetryBuilder { - public OpenTelemetry.IOpenTelemetryBuilder WithTracing(System.Action configure) => this; - } - """; - - [Fact] - public Task ShouldReportWhenNoSamplerConfigured() => VerifyAsync($$""" - {{OtelStubs}} - - public class C { - void M(TestOtelBuilder builder) { - builder.{|AL0092:WithTracing|}(tracing => tracing.AddSource("MyService")); - } - } - """); - - [Fact] - public Task ShouldReportWhenAlwaysOnSamplerUsed() => VerifyAsync($$""" - {{OtelStubs}} - - public class C { - void M(TestOtelBuilder builder) { - builder.{|AL0092:WithTracing|}(tracing => tracing - .AddSource("MyService") - .SetSampler(new OpenTelemetry.Trace.AlwaysOnSampler())); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenParentBasedSamplerUsed() => VerifyAsync($$""" - {{OtelStubs}} - - public class C { - void M(TestOtelBuilder builder) { - builder.WithTracing(tracing => tracing - .AddSource("MyService") - .SetSampler(new OpenTelemetry.Trace.ParentBasedSampler( - new OpenTelemetry.Trace.TraceIdRatioBasedSampler(0.1)))); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenTraceIdRatioBasedSamplerUsed() => VerifyAsync($$""" - {{OtelStubs}} - - public class C { - void M(TestOtelBuilder builder) { - builder.WithTracing(tracing => tracing - .AddSource("MyService") - .SetSampler(new OpenTelemetry.Trace.TraceIdRatioBasedSampler(0.5))); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenAlwaysOffSamplerUsed() => VerifyAsync($$""" - {{OtelStubs}} - - public class C { - void M(TestOtelBuilder builder) { - builder.WithTracing(tracing => tracing - .AddSource("MyService") - .SetSampler(new OpenTelemetry.Trace.AlwaysOffSampler())); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenNoOtelTypes() => VerifyAsync( - """ - public class C { - void WithTracing(System.Action configure) { } - void M() { - WithTracing(t => { }); - } - } - """); - - [Fact] - public Task ShouldNotReportForNonTracingMethods() => VerifyAsync($$""" - {{OtelStubs}} - - namespace OpenTelemetry.Metrics { - public abstract class MeterProviderBuilder { - public MeterProviderBuilder WithMetrics(System.Action configure) => this; - } - } - - public class TestMeterBuilder : OpenTelemetry.Metrics.MeterProviderBuilder { } - - public class C { - void M(TestMeterBuilder builder) { - // WithMetrics is not a tracing method - should not report - builder.WithMetrics(metrics => { }); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0093MissingResourceAttributesTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0093MissingResourceAttributesTests.cs deleted file mode 100644 index 2f645a9..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0093MissingResourceAttributesTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0093: Detects missing resource attributes in OpenTelemetry configuration. -/// -public sealed partial class Al0093MissingResourceAttributesTests : AnalyzerTest { - [Theory] - [InlineData("AddOpenTelemetry")] - [InlineData("UseOpenTelemetry")] - [InlineData("ConfigureOpenTelemetry")] - public Task ShouldReportWhenOTelConfiguredWithoutResourceAttributes(string methodName) => VerifyAsync($$""" - public static class OTelExtensions { - public static void {{methodName}}(this object builder) { } - } - - public class C { - void Configure() { - var builder = new object(); - builder.[|{{methodName}}|](); - } - } - """); - - [Theory] - [InlineData("AddOpenTelemetry", "ConfigureResource")] - [InlineData("AddOpenTelemetry", "AddResource")] - [InlineData("AddOpenTelemetry", "AddService")] - [InlineData("AddOpenTelemetry", "SetResourceBuilder")] - [InlineData("UseOpenTelemetry", "ConfigureResource")] - [InlineData("ConfigureOpenTelemetry", "AddService")] - public Task ShouldNotReportWhenResourceConfigured(string otelMethod, string resourceMethod) => VerifyAsync($$""" - public static class OTelExtensions { - public static object {{otelMethod}}(this object builder) => builder; - public static object {{resourceMethod}}(this object builder) => builder; - } - - public class C { - void Configure() { - var builder = new object(); - builder.{{otelMethod}}().{{resourceMethod}}(); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenNoOTelSetup() => VerifyAsync(""" - public class C { - void Configure() { - var builder = new object(); - builder.ToString(); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenResourceConfiguredSeparately() => VerifyAsync(""" - public static class OTelExtensions { - public static void AddOpenTelemetry(this object builder) { } - public static void ConfigureResource(this object builder) { } - } - - public class C { - void Configure() { - var builder = new object(); - builder.AddOpenTelemetry(); - builder.ConfigureResource(); - } - } - """); - - [Fact] - public Task ShouldReportOnlyOnOTelSetupMethod() => VerifyAsync(""" - public static class OTelExtensions { - public static void AddOpenTelemetry(this object builder) { } - public static void SomeOtherMethod(this object builder) { } - } - - public class C { - void Configure() { - var builder = new object(); - builder.[|AddOpenTelemetry|](); - builder.SomeOtherMethod(); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0096EnableEventSourceSupportTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0096EnableEventSourceSupportTests.cs deleted file mode 100644 index 913df22..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0096EnableEventSourceSupportTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0096: Enable EventSourceSupport for AOT with telemetry. -/// -/// -/// This analyzer reads MSBuild properties via AnalyzerConfigOptionsProvider.GlobalOptions. -/// In unit tests without a .globalconfig, the properties are not available, so the analyzer -/// correctly produces no diagnostics (PublishAot is not set). The positive case (PublishAot=true -/// without EventSourceSupport=true) is verified via integration tests at build time when the -/// MSBuild properties are exposed through CompilerVisibleProperty in ANcpLua.Analyzers.props. -/// -public sealed partial class Al0096EnableEventSourceSupportTests : AnalyzerTest { - [Fact] - public Task ShouldNotReportWhenPublishAotIsNotSet() => - VerifyAsync(""" - public class C { - public void M() { } - } - """); - - [Fact] - public Task ShouldNotReportForEmptyCode() => - VerifyAsync("public class C { }"); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0107OrphanedTracedTagTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0107OrphanedTracedTagTests.cs deleted file mode 100644 index 5858afe..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0107OrphanedTracedTagTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0107: Orphaned [TracedTag] - tag on parameter without [Traced] in scope. -/// -public sealed partial class Al0107OrphanedTracedTagTests : AnalyzerTest { - private const string Stubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)] - public class TracedAttribute : System.Attribute { - public TracedAttribute() { } - public TracedAttribute(string activitySourceName) { } - public string ActivitySourceName { get; set; } - } - - [System.AttributeUsage(System.AttributeTargets.Parameter)] - public class TracedTagAttribute : System.Attribute { - public TracedTagAttribute() { } - public TracedTagAttribute(string name) { } - } - } - """; - - [Fact] - public Task ShouldReportWhenNoTracedOnMethodOrClass() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - public void Process([Qyl.Instrumentation.Instrumentation.TracedTag] string {|AL0107:orderId|}) { } - } - """); - - [Fact] - public Task ShouldNotReportWhenMethodHasTraced() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void Process([Qyl.Instrumentation.Instrumentation.TracedTag] string orderId) { } - } - """); - - [Fact] - public Task ShouldNotReportWhenClassHasTraced() => - VerifyAsync($$""" - {{Stubs}} - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public class OrderService { - public void Process([Qyl.Instrumentation.Instrumentation.TracedTag] string orderId) { } - } - """); - - [Fact] - public Task ShouldNotReportWhenBaseClassHasTraced() => - VerifyAsync($$""" - {{Stubs}} - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public class BaseService { } - - public class OrderService : BaseService { - public void Process([Qyl.Instrumentation.Instrumentation.TracedTag] string orderId) { } - } - """); - - [Fact] - public Task ShouldReportMultipleOrphanedTags() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - public void Process( - [Qyl.Instrumentation.Instrumentation.TracedTag] string {|AL0107:orderId|}, - [Qyl.Instrumentation.Instrumentation.TracedTag] int {|AL0107:count|}) { } - } - """); - - [Fact] - public Task ShouldNotReportParameterWithoutTracedTag() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - public void Process(string orderId) { } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0108RedundantNoTraceTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0108RedundantNoTraceTests.cs deleted file mode 100644 index 6c9065f..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0108RedundantNoTraceTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0108: Redundant [NoTrace] - method has [NoTrace] but declaring type has no [Traced]. -/// -public sealed partial class Al0108RedundantNoTraceTests : AnalyzerTest { - private const string Stubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)] - public class TracedAttribute : System.Attribute { - public TracedAttribute() { } - public TracedAttribute(string activitySourceName) { } - public string ActivitySourceName { get; set; } - } - - [System.AttributeUsage(System.AttributeTargets.Method)] - public class NoTraceAttribute : System.Attribute { } - } - """; - - [Fact] - public Task ShouldReportWhenNoTracedOnClass() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.NoTrace] - public void {|AL0108:HelperMethod|}() { } - } - """); - - [Fact] - public Task ShouldNotReportWhenClassHasTraced() => - VerifyAsync($$""" - {{Stubs}} - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public class OrderService { - [Qyl.Instrumentation.Instrumentation.NoTrace] - public void HelperMethod() { } - } - """); - - [Fact] - public Task ShouldNotReportWhenBaseClassHasTraced() => - VerifyAsync($$""" - {{Stubs}} - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public class BaseService { } - - public class OrderService : BaseService { - [Qyl.Instrumentation.Instrumentation.NoTrace] - public void HelperMethod() { } - } - """); - - [Fact] - public Task ShouldNotReportMethodWithoutNoTrace() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - public void HelperMethod() { } - } - """); - - [Fact] - public Task ShouldReportWhenNoTracedOnClassOrBaseClasses() => - VerifyAsync($$""" - {{Stubs}} - - public class BaseService { } - - public class OrderService : BaseService { - [Qyl.Instrumentation.Instrumentation.NoTrace] - public void {|AL0108:HelperMethod|}() { } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0109NonInterceptableTracedTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0109NonInterceptableTracedTests.cs deleted file mode 100644 index 26ebca0..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0109NonInterceptableTracedTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0109: Non-interceptable [Traced] on abstract, extern, or partial definition methods. -/// -public sealed partial class Al0109NonInterceptableTracedTests : AnalyzerTest { - private const string Stubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)] - public class TracedAttribute : System.Attribute { - public TracedAttribute() { } - public TracedAttribute(string activitySourceName) { } - public string ActivitySourceName { get; set; } - } - } - """; - - [Fact] - public Task ShouldReportOnAbstractMethod() => - VerifyAsync($$""" - {{Stubs}} - - public abstract class BaseService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public abstract void {|AL0109:ProcessAsync|}(); - } - """); - - [Fact] - public Task ShouldReportOnExternMethod() => - VerifyAsync($$""" - using System.Runtime.InteropServices; - {{Stubs}} - - public class NativeService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - [DllImport("native.dll")] - public static extern void {|AL0109:NativeCall|}(); - } - """); - - [Fact] - public Task ShouldReportOnPartialDefinitionMethod() => - VerifyAsync($$""" - {{Stubs}} - - public partial class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public partial void {|AL0109:Process|}(); - } - - public partial class OrderService { - public partial void Process() { } - } - """); - - [Fact] - public Task ShouldNotReportOnConcreteMethod() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void Process() { } - } - """); - - [Fact] - public Task ShouldNotReportOnMethodWithoutTracedAttribute() => - VerifyAsync($$""" - {{Stubs}} - - public abstract class BaseService { - public abstract void ProcessAsync(); - } - """); - - [Fact] - public Task ShouldNotReportOnClassLevelTracedWithAbstractMethod() => - VerifyAsync($$""" - {{Stubs}} - - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public abstract class BaseService { - public abstract void ProcessAsync(); - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0110TracedTagOnOutRefParameterTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0110TracedTagOnOutRefParameterTests.cs deleted file mode 100644 index 3e9e83f..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0110TracedTagOnOutRefParameterTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0110: [TracedTag] on out or ref parameters. -/// -public sealed partial class Al0110TracedTagOnOutRefParameterTests : AnalyzerTest { - private const string Stubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)] - public class TracedAttribute : System.Attribute { - public TracedAttribute() { } - public TracedAttribute(string activitySourceName) { } - public string ActivitySourceName { get; set; } - } - - [System.AttributeUsage(System.AttributeTargets.Parameter)] - public class TracedTagAttribute : System.Attribute { - public TracedTagAttribute() { } - public TracedTagAttribute(string name) { } - } - } - """; - - [Fact] - public Task ShouldReportOnOutParameter() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public bool TryParse(string input, [Qyl.Instrumentation.Instrumentation.TracedTag] out int {|AL0110:result|}) { - result = 0; - return true; - } - } - """); - - [Fact] - public Task ShouldReportOnRefParameter() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void Update([Qyl.Instrumentation.Instrumentation.TracedTag] ref int {|AL0110:value|}) { - value = 42; - } - } - """); - - [Fact] - public Task ShouldNotReportOnNormalParameter() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void Process([Qyl.Instrumentation.Instrumentation.TracedTag] string orderId) { } - } - """); - - [Fact] - public Task ShouldNotReportOnParameterWithoutTracedTag() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public bool TryParse(string input, out int result) { - result = 0; - return true; - } - } - """); - - [Fact] - public Task ShouldReportOnOutParameterEvenWithoutTracedOnMethod() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - public bool TryParse(string input, [Qyl.Instrumentation.Instrumentation.TracedTag] out int {|AL0110:result|}) { - result = 0; - return true; - } - } - """); - - [Fact] - public Task ShouldNotReportOnInParameter() => - VerifyAsync($$""" - {{Stubs}} - - public class OrderService { - [Qyl.Instrumentation.Instrumentation.Traced("MyApp")] - public void Process([Qyl.Instrumentation.Instrumentation.TracedTag] in int value) { } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0113MissingExceptionRecordingOnActivityTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0113MissingExceptionRecordingOnActivityTests.cs deleted file mode 100644 index 0171c7f..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0113MissingExceptionRecordingOnActivityTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0113: Missing exception recording on Activity error status. -/// -public sealed partial class Al0113MissingExceptionRecordingOnActivityTests - : AnalyzerTest { - private const string ActivityStubs = """ - namespace System.Diagnostics { - public enum ActivityStatusCode { Unset, Ok, Error } - public class ActivityEvent { - public ActivityEvent(string name) { } - } - public class Activity { - public void SetStatus(ActivityStatusCode code, string? desc = null) { } - public void AddEvent(ActivityEvent e) { } - public void AddException(System.Exception ex) { } - } - } - """; - - [Fact] - public Task ShouldReport_SetStatusErrorWithoutExceptionRecording() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - public void M(System.Diagnostics.Activity activity) { - try { throw new System.Exception(); } - catch { - [|activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, "fail")|]; - } - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenAddEventWithExceptionCalled() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - public void M(System.Diagnostics.Activity activity) { - try { throw new System.Exception(); } - catch (System.Exception ex) { - activity.AddEvent(new System.Diagnostics.ActivityEvent("exception")); - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message); - } - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenStatusIsOk() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - public void M(System.Diagnostics.Activity activity) { - try { } - catch { - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Ok); - } - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenSetStatusOutsideCatch() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - public void M(System.Diagnostics.Activity activity) { - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, "fail"); - } - } - """); - - [Fact] - public Task ShouldNotReport_WhenAddEventAfterSetStatus() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - public void M(System.Diagnostics.Activity activity) { - try { throw new System.Exception(); } - catch (System.Exception ex) { - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message); - activity.AddEvent(new System.Diagnostics.ActivityEvent("exception")); - } - } - } - """); - - // AddException is the .NET 9+ counterpart to OTel's RecordException — it records the - // exception as an ActivityEvent with the standard "exception" name and captures - // type/message/stack as tags. The analyzer must treat it as exception recording. - [Fact] - public Task ShouldNotReport_WhenAddExceptionCalled() => - VerifyAsync($$""" - {{ActivityStubs}} - - public class C { - public void M(System.Diagnostics.Activity activity) { - try { throw new System.Exception(); } - catch (System.Exception ex) { - activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message); - activity.AddException(ex); - } - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0124NonInterceptableAgentTracedTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0124NonInterceptableAgentTracedTests.cs deleted file mode 100644 index 3400de9..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0124NonInterceptableAgentTracedTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Analyzers.CodeFixes.CodeFixes; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0124: Non-interceptable [AgentTraced] on abstract, extern, or partial definition methods. -/// -public sealed partial class Al0124NonInterceptableAgentTracedTests : AnalyzerTest { - private const string AgentStubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Method)] - public class AgentTracedAttribute : System.Attribute { - public AgentTracedAttribute() { } - public string AgentName { get; set; } - } - } - """; - - [Fact] - public Task ShouldReportOnAbstractMethod() => - VerifyAsync($$""" - {{AgentStubs}} - - public abstract class BaseAgent { - [Qyl.Instrumentation.Instrumentation.AgentTraced(AgentName = "test")] - public abstract void {|AL0124:InvokeAsync|}(); - } - """); - - [Fact] - public Task ShouldReportOnExternMethod() => - VerifyAsync($$""" - using System.Runtime.InteropServices; - {{AgentStubs}} - - public class NativeAgent { - [Qyl.Instrumentation.Instrumentation.AgentTraced] - [DllImport("native.dll")] - public static extern void {|AL0124:NativeCall|}(); - } - """); - - [Fact] - public Task ShouldReportOnPartialDefinitionMethod() => - VerifyAsync($$""" - {{AgentStubs}} - - public partial class AgentService { - [Qyl.Instrumentation.Instrumentation.AgentTraced] - public partial void {|AL0124:Process|}(); - } - - public partial class AgentService { - public partial void Process() { } - } - """); - - [Fact] - public Task ShouldNotReportOnConcreteMethod() => - VerifyAsync($$""" - {{AgentStubs}} - - public class AgentService { - [Qyl.Instrumentation.Instrumentation.AgentTraced(AgentName = "test")] - public void Process() { } - } - """); - - [Fact] - public Task ShouldNotReportOnMethodWithoutAttribute() => - VerifyAsync($$""" - {{AgentStubs}} - - public abstract class BaseAgent { - public abstract void InvokeAsync(); - } - """); - - [Fact] - public Task ShouldNotReportWhenAttributeNotReferenced() => - VerifyAsync(""" - public abstract class BaseAgent { - public abstract void InvokeAsync(); - } - """); -} - -/// -/// Code fix tests for AL0124: Removes [AgentTraced] attribute from non-interceptable methods. -/// -public sealed partial class Al0124CodeFixTests : CodeFixTest { - private const string AgentStubs = """ - namespace Qyl.Instrumentation.Instrumentation { - [System.AttributeUsage(System.AttributeTargets.Method)] - public class AgentTracedAttribute : System.Attribute { - public AgentTracedAttribute() { } - public string AgentName { get; set; } - } - } - """; - - [Fact] - public Task ShouldRemoveAgentTracedFromAbstractMethod() => - VerifyAsync($$""" - {{AgentStubs}} - - public abstract class BaseAgent { - [Qyl.Instrumentation.Instrumentation.AgentTraced] - public abstract void {|AL0124:InvokeAsync|}(); - } - """, - $$""" - {{AgentStubs}} - - public abstract class BaseAgent { - public abstract void InvokeAsync(); - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0131DirectGenAiSdkUsageTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0131DirectGenAiSdkUsageTests.cs deleted file mode 100644 index 6d6dad0..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0131DirectGenAiSdkUsageTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using AnalyzerTestBase = ANcpLua.Roslyn.Utilities.Testing.AnalyzerTest; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0131: Direct GenAI SDK usage bypassing IChatClient. -/// -/// -/// The rule MUST fire on direct provider SDK invocations (OpenAI, Anthropic, Ollama, etc.) -/// and MUST NOT fire on calls to the Microsoft.Agents.AI abstractions (AIAgent, -/// ChatClientAgent, DelegatingAIAgent). Those are the abstraction, not the bypass. -/// -public sealed partial class Al0131DirectGenAiSdkUsageTests : AnalyzerTestBase -{ - [Fact] - public Task ShouldNotReportAIAgentRunAsync() => - VerifyAsync(""" - namespace Microsoft.Agents.AI { - public abstract class AIAgent { - public virtual System.Threading.Tasks.Task RunAsync() => System.Threading.Tasks.Task.CompletedTask; - } - } - - public class C { - public async System.Threading.Tasks.Task M(Microsoft.Agents.AI.AIAgent agent) { - await agent.RunAsync(); - } - } - """); - - [Fact] - public Task ShouldNotReportChatClientAgentRunAsync() => - VerifyAsync(""" - namespace Microsoft.Agents.AI { - public class ChatClientAgent { - public virtual System.Threading.Tasks.Task RunAsync() => System.Threading.Tasks.Task.CompletedTask; - } - } - - public class C { - public async System.Threading.Tasks.Task M(Microsoft.Agents.AI.ChatClientAgent agent) { - await agent.RunAsync(); - } - } - """); - - [Fact] - public Task ShouldNotReportDelegatingAIAgentRunAsync() => - VerifyAsync(""" - namespace Microsoft.Agents.AI { - public class DelegatingAIAgent { - public virtual System.Threading.Tasks.Task RunAsync() => System.Threading.Tasks.Task.CompletedTask; - } - } - - public class C { - public async System.Threading.Tasks.Task M(Microsoft.Agents.AI.DelegatingAIAgent agent) { - await agent.RunAsync(); - } - } - """); - - [Fact] - public Task ShouldReportDirectOpenAIChatClientCall() => - VerifyAsync(""" - namespace OpenAI.Chat { - public class ChatClient { - public System.Threading.Tasks.Task CompleteChatAsync() => System.Threading.Tasks.Task.CompletedTask; - } - } - - public class C { - public async System.Threading.Tasks.Task M(OpenAI.Chat.ChatClient client) { - await {|AL0131:client.CompleteChatAsync()|}; - } - } - """); - - [Fact] - public Task ShouldReportDirectAnthropicCall() => - VerifyAsync(""" - namespace Anthropic { - public class AnthropicClient { - public System.Threading.Tasks.Task CreateMessageAsync() => System.Threading.Tasks.Task.CompletedTask; - } - } - - public class C { - public async System.Threading.Tasks.Task M(Anthropic.AnthropicClient client) { - await {|AL0131:client.CreateMessageAsync()|}; - } - } - """); - - [Fact] - public Task ShouldNotReportIChatClientCall() => - VerifyAsync(""" - namespace Microsoft.Extensions.AI { - public interface IChatClient { - System.Threading.Tasks.Task GetResponseAsync(); - } - } - - public class C { - public async System.Threading.Tasks.Task M(Microsoft.Extensions.AI.IChatClient client) { - await client.GetResponseAsync(); - } - } - """); -} diff --git a/tests/ANcpLua.Analyzers.Tests/AL0135LegacySemanticConventionsAccessorTests.cs b/tests/ANcpLua.Analyzers.Tests/AL0135LegacySemanticConventionsAccessorTests.cs deleted file mode 100644 index 5a9d4f5..0000000 --- a/tests/ANcpLua.Analyzers.Tests/AL0135LegacySemanticConventionsAccessorTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using ANcpLua.Analyzers.Analyzers; -using ANcpLua.Roslyn.Utilities.Testing; - -namespace ANcpLua.Analyzers.Tests; - -/// -/// Tests for AL0135: flag legacy aggregated SemanticConventions accessors. -/// -public sealed partial class Al0135LegacySemanticConventionsAccessorTests - : AnalyzerTest { - private const string LegacyAggregatorStub = """ - namespace OpenTelemetry.SemanticConventions { - public static class SemanticConventions { - public const string AttributeHttpMethod = "http.method"; - public const string AttributeHttpStatusCode = "http.status_code"; - } - } - """; - - private const string LegacyTraceStub = """ - namespace OpenTelemetry.Trace { - public static class TraceSemanticConventions { - public const string AttributeNetPeerName = "net.peer.name"; - } - } - """; - - private const string GroupedStub = """ - namespace OpenTelemetry.SemanticConventions.Attributes { - public static class HttpAttributes { - public const string AttributeHttpRequestMethod = "http.request.method"; - } - } - """; - - [Fact] - public Task ShouldReportLegacyAggregatorMemberAccess() => - VerifyAsync($$""" - using System.Diagnostics; - using OpenTelemetry.SemanticConventions; - {{LegacyAggregatorStub}} - - public class C { - public void M(Activity activity) { - activity.SetTag([|SemanticConventions.AttributeHttpMethod|], "GET"); - } - } - """); - - [Fact] - public Task ShouldReportLegacyTraceAccessorMemberAccess() => - VerifyAsync($$""" - using System.Diagnostics; - using OpenTelemetry.Trace; - {{LegacyTraceStub}} - - public class C { - public void M(Activity activity) { - activity.SetTag([|TraceSemanticConventions.AttributeNetPeerName|], "localhost"); - } - } - """); - - [Fact] - public Task ShouldNotReportGroupedAttributeAccessor() => - VerifyAsync($$""" - using System.Diagnostics; - using OpenTelemetry.SemanticConventions.Attributes; - {{GroupedStub}} - - public class C { - public void M(Activity activity) { - activity.SetTag(HttpAttributes.AttributeHttpRequestMethod, "GET"); - } - } - """); - - [Fact] - public Task ShouldNotReportWhenPackageAbsent() => - VerifyAsync(""" - using System.Diagnostics; - - public class C { - public void M(Activity activity) { - activity.SetTag("http.method", "GET"); - } - } - """); -} From 09c536f40ba223b1e4694bf7adb7a9a01131c69c Mon Sep 17 00:00:00 2001 From: ancplua Date: Fri, 8 May 2026 02:03:25 +0200 Subject: [PATCH 2/4] chore: update analyzer release tracking after OTel migration --- .../AnalyzerReleases.Unshipped.md | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md b/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md index 3a75242..43b7ee9 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,8 @@ 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 From be79c1d45f8b31428e02f9ecbf7c69acbb11d651 Mon Sep 17 00:00:00 2001 From: ancplua Date: Fri, 8 May 2026 02:06:01 +0200 Subject: [PATCH 3/4] docs: align analyzer metadata after OTSC move --- CHANGELOG.md | 9 +-- README.md | 59 +++---------------- .../ANcpLua.Analyzers.csproj | 2 +- 3 files changed, 11 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2196ffd..2f4e642 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, AL0128, AL0129, AL0130, AL0131, AL0135 - `verify-published-versions.ps1` — superseded by AL0017/AL0018/AL0019 compile-time enforcement diff --git a/README.md b/README.md index 7750192..92e14f0 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, and ANcpLua ecosystem conventions. ## Installation @@ -16,27 +16,24 @@ 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. +- **85 diagnostics** spanning design, reliability, usage, Roslyn utilities, ASP.NET Core, AOT, threading, style, configuration, 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 | | Style | 3 | +| Configuration | 1 | ## Full rule catalog @@ -52,7 +49,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 +73,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 +95,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 +119,11 @@ 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 +131,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/ANcpLua.Analyzers.csproj b/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj index 262df82..078ce03 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. + 85 Roslyn diagnostics for modern C# — async pitfalls, AOT safety, ASP.NET Core patterns, reliability, Roslyn utilities, and version management. Includes 36 automatic code fixes. false From 38a14bdf18755814f741bdbcb35bb69fac8b5185 Mon Sep 17 00:00:00 2001 From: ancplua Date: Fri, 8 May 2026 02:07:38 +0200 Subject: [PATCH 4/4] fix: keep tool governance analyzers in core --- CHANGELOG.md | 2 +- README.md | 8 +- .../ANcpLua.Analyzers.csproj | 2 +- .../AnalyzerReleases.Unshipped.md | 3 + ...tructiveToolMustRequireApprovalAnalyzer.cs | 106 ++++++++++++++++++ ...AL0129ToolMustDeclareSideEffectAnalyzer.cs | 62 ++++++++++ ...AL0130ToolMustDeclareCapabilityAnalyzer.cs | 63 +++++++++++ 7 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs create mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs create mode 100644 src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f4e642..cb4a5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,5 +23,5 @@ All notable changes to ANcpLua.Analyzers will be documented in this file. ### 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, AL0128, AL0129, AL0130, AL0131, AL0135 +- 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 92e14f0..086dba3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # ANcpLua.Analyzers -Roslyn analyzers for modern C# patterns, AOT safety, ASP.NET Core, reliability, 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,7 +16,7 @@ dotnet add package ANcpLua.Analyzers ## What you get -- **85 diagnostics** spanning design, reliability, usage, Roslyn utilities, ASP.NET Core, AOT, threading, style, configuration, and version management. +- **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. @@ -32,6 +32,7 @@ dotnet add package ANcpLua.Analyzers | VersionManagement | 7 | | Design | 6 | | Threading | 6 | +| GenAI | 3 | | Style | 3 | | Configuration | 1 | @@ -122,6 +123,9 @@ dotnet add package ANcpLua.Analyzers | [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` | | [AL0137](https://ancplua.mintlify.app/analyzers/rules/AL0137) | Roslyn Utilities | Warning | `Al0137UseGuardForThrowIfAnalyzer` | | [AL0138](https://ancplua.mintlify.app/analyzers/rules/AL0138) | Reliability | Warning | `Al0138UseExplicitMidpointRoundingAnalyzer` | diff --git a/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj b/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj index 078ce03..2d97296 100644 --- a/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj +++ b/src/ANcpLua.Analyzers/ANcpLua.Analyzers.csproj @@ -18,7 +18,7 @@ true en README.md - 85 Roslyn diagnostics for modern C# — async pitfalls, AOT safety, ASP.NET Core patterns, reliability, Roslyn utilities, and version management. Includes 36 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 43b7ee9..4dcf644 100644 --- a/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/ANcpLua.Analyzers/AnalyzerReleases.Unshipped.md @@ -88,5 +88,8 @@ AL0123 | Design | Warning | Al0123DuckDbColumnConflictingOrdinalAnalyzer 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 AL0137 | Roslyn Utilities | Warning | Al0137UseGuardForThrowIfAnalyzer AL0138 | Reliability | Warning | Al0138UseExplicitMidpointRoundingAnalyzer diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs new file mode 100644 index 0000000..1cc08d3 --- /dev/null +++ b/src/ANcpLua.Analyzers/Analyzers/AL0128DestructiveToolMustRequireApprovalAnalyzer.cs @@ -0,0 +1,106 @@ + +namespace ANcpLua.Analyzers.Analyzers; + +/// +/// AL0128: Detects Loom tools with destructive side effects that do not require approval. +/// +/// +/// +/// When a Loom tool has a destructive side effect (WritesExternalState, MutatesCode, +/// Deploys, or ClosesIssue), it must also declare [RequiresApproval] to ensure +/// governance gates are in place before execution. +/// +/// +/// The ToolSideEffect enum values considered destructive are: +/// +/// WritesExternalState (2) +/// MutatesCode (3) +/// Deploys (4) +/// ClosesIssue (5) +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed partial class Al0128DestructiveToolMustRequireApprovalAnalyzer : AlAnalyzer { + /// The diagnostic identifier for AL0128. + private const string DiagnosticId = "AL0128"; + + private const string LoomToolAttributeName = "LoomTool"; + private const string ToolSideEffectAttributeName = "ToolSideEffect"; + private const string RequiresApprovalAttributeName = "RequiresApproval"; + + /// Minimum enum value that is considered destructive (WritesExternalState = 2). + private const int DestructiveSideEffectThreshold = 2; + + private static readonly string[] s_destructiveSideEffectNames = [ + "WritesExternalState", + "MutatesCode", + "Deploys", + "ClosesIssue" + ]; + + 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 [LoomTool] for missing [RequiresApproval]. + protected override void RegisterActions(AnalysisContext context) => + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + + private static void AnalyzeMethod(SymbolAnalysisContext context) { + var method = (IMethodSymbol)context.Symbol; + + if (!method.HasAttributeByShortName(LoomToolAttributeName)) { + return; + } + + if (HasAttributeOnMethodOrType(method, RequiresApprovalAttributeName)) { + return; + } + + if (GetDestructiveSideEffectName(method) is not { } destructiveSideEffect) { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + s_rule, + method.Locations.FirstOrDefault() ?? Location.None, + method.Name, + destructiveSideEffect)); + } + + private static string? GetDestructiveSideEffectName(IMethodSymbol method) { + var result = FindDestructiveSideEffectInAttributes(method.GetAttributes()); + + return result ?? FindDestructiveSideEffectInAttributes(method.ContainingType?.GetAttributes()); + } + + private static string? FindDestructiveSideEffectInAttributes(ImmutableArray? attributes) { + if (attributes is not { } attrs) { + return null; + } + + foreach (var attribute in attrs) { + if (attribute.AttributeClass?.Name is not ToolSideEffectAttributeName and + not "ToolSideEffectAttribute") { + continue; + } + + if (attribute.ConstructorArguments is [{ Value: int enumValue and >= DestructiveSideEffectThreshold }, ..]) { + return enumValue < DestructiveSideEffectThreshold + s_destructiveSideEffectNames.Length + ? s_destructiveSideEffectNames[enumValue - DestructiveSideEffectThreshold] + : enumValue.ToString(); + } + } + + return null; + } + + private static bool HasAttributeOnMethodOrType(IMethodSymbol method, string attributeName) => + method.HasAttributeByShortName(attributeName) || + method.ContainingType?.HasAttributeByShortName(attributeName) is true; +} diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs new file mode 100644 index 0000000..9efd37d --- /dev/null +++ b/src/ANcpLua.Analyzers/Analyzers/AL0129ToolMustDeclareSideEffectAnalyzer.cs @@ -0,0 +1,62 @@ + +namespace ANcpLua.Analyzers.Analyzers; + +/// +/// AL0129: Detects Loom tools that do not declare their side effect. +/// +/// +/// +/// Every method annotated with [LoomTool] should also have [ToolSideEffect] on the +/// method itself or on its containing type. This makes the governance surface explicit +/// and enables automated policy enforcement. +/// +/// +/// Example of correct usage: +/// +/// [LoomTool] +/// [ToolSideEffect(ToolSideEffects.ReadsOnly)] +/// public Task<string> ReadFile(string path) { ... } +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed partial class Al0129ToolMustDeclareSideEffectAnalyzer : AlAnalyzer { + /// The diagnostic identifier for AL0129. + private const string DiagnosticId = "AL0129"; + + private const string LoomToolAttributeName = "LoomTool"; + private const string ToolSideEffectAttributeName = "ToolSideEffect"; + + private static readonly DiagnosticDescriptor s_rule = CreateRule( + DiagnosticId, + DiagnosticCategories.GenAI, + DiagnosticSeverities.HiddenByDefault); + + /// Gets the diagnostic descriptors for the supported diagnostics. + public override ImmutableArray SupportedDiagnostics => [s_rule]; + + /// Registers symbol actions to analyze methods with [LoomTool] for missing [ToolSideEffect]. + protected override void RegisterActions(AnalysisContext context) => + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + + private static void AnalyzeMethod(SymbolAnalysisContext context) { + var method = (IMethodSymbol)context.Symbol; + + if (!method.HasAttributeByShortName(LoomToolAttributeName)) { + return; + } + + if (HasAttributeOnMethodOrType(method, ToolSideEffectAttributeName)) { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + s_rule, + method.Locations.FirstOrDefault() ?? Location.None, + method.Name)); + } + + private static bool HasAttributeOnMethodOrType(IMethodSymbol method, string attributeName) => + method.HasAttributeByShortName(attributeName) || + method.ContainingType?.HasAttributeByShortName(attributeName) is true; +} diff --git a/src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs b/src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs new file mode 100644 index 0000000..e470913 --- /dev/null +++ b/src/ANcpLua.Analyzers/Analyzers/AL0130ToolMustDeclareCapabilityAnalyzer.cs @@ -0,0 +1,63 @@ + +namespace ANcpLua.Analyzers.Analyzers; + +/// +/// AL0130: Detects Loom tools that do not declare any required capabilities. +/// +/// +/// +/// Every method annotated with [LoomTool] should also have at least one +/// [RequiresCapability] attribute on the method itself or on its containing type. +/// This enables fine-grained access control and ensures that the tool's permission +/// requirements are visible at compile time. +/// +/// +/// Example of correct usage: +/// +/// [LoomTool] +/// [RequiresCapability("file:read")] +/// public Task<string> ReadFile(string path) { ... } +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed partial class Al0130ToolMustDeclareCapabilityAnalyzer : AlAnalyzer { + /// The diagnostic identifier for AL0130. + private const string DiagnosticId = "AL0130"; + + private const string LoomToolAttributeName = "LoomTool"; + private const string RequiresCapabilityAttributeName = "RequiresCapability"; + + private static readonly DiagnosticDescriptor s_rule = CreateRule( + DiagnosticId, + DiagnosticCategories.GenAI, + DiagnosticSeverities.HiddenByDefault); + + /// Gets the diagnostic descriptors for the supported diagnostics. + public override ImmutableArray SupportedDiagnostics => [s_rule]; + + /// Registers symbol actions to analyze methods with [LoomTool] for missing [RequiresCapability]. + protected override void RegisterActions(AnalysisContext context) => + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + + private static void AnalyzeMethod(SymbolAnalysisContext context) { + var method = (IMethodSymbol)context.Symbol; + + if (!method.HasAttributeByShortName(LoomToolAttributeName)) { + return; + } + + if (HasAttributeOnMethodOrType(method, RequiresCapabilityAttributeName)) { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + s_rule, + method.Locations.FirstOrDefault() ?? Location.None, + method.Name)); + } + + private static bool HasAttributeOnMethodOrType(IMethodSymbol method, string attributeName) => + method.HasAttributeByShortName(attributeName) || + method.ContainingType?.HasAttributeByShortName(attributeName) is true; +}