Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5f5d12f
Add keyed service diagnostics
michaelstaib Aug 29, 2026
9a5dd45
Support keyed DataLoader services
michaelstaib Aug 29, 2026
28ba432
Preserve keyed service inspector identity
michaelstaib Aug 29, 2026
259ccf2
Restore DataLoader service coverage
michaelstaib Aug 29, 2026
143466b
Detect keyed service attributes
michaelstaib Aug 29, 2026
a32f71b
Fix derived service key resolution
michaelstaib Aug 29, 2026
1f271d8
Resolve implicit service attribute constructors
michaelstaib Aug 29, 2026
5b4b422
Resolve optional implicit base constructors
michaelstaib Aug 29, 2026
e52d31a
Emit keyed DataLoader services
michaelstaib Aug 29, 2026
bc2bd12
Detect conflicting keyed DataLoader services
michaelstaib Aug 29, 2026
ef5d8c0
Consolidate HC0130 location assertion
michaelstaib Aug 29, 2026
58e0aa9
Add keyed DataLoader service attribute analyzer
michaelstaib Aug 29, 2026
71f7ecb
Preserve keyed service attribute trivia
michaelstaib Aug 29, 2026
a6baa91
Fix keyed service attribute classification
michaelstaib Aug 29, 2026
fa6109f
Add keyed DataLoader constructor analyzer
michaelstaib Aug 29, 2026
2be849a
Fix keyed service constructor code fix
michaelstaib Aug 30, 2026
d8385d4
Add keyed service key analyzer
michaelstaib Aug 30, 2026
7c461ed
Document keyed DataLoader services
michaelstaib Aug 30, 2026
3f8118b
Clarify keyed DataLoader service diagnostics
michaelstaib Aug 30, 2026
72d7f82
Add keyed DataLoader integration tests
michaelstaib Aug 30, 2026
6b93ea0
Separate keyed DataLoader test phases
michaelstaib Aug 30, 2026
83adb6e
Add object service keys
michaelstaib Aug 30, 2026
07c2736
Fix DataLoader documentation linting
michaelstaib Aug 30, 2026
c2329de
Format DataLoader diagnostics table
michaelstaib Aug 30, 2026
b5d75e7
Support object service keys in analyzers
michaelstaib Aug 30, 2026
482252b
Fix enum keyed service coverage
michaelstaib Aug 30, 2026
fd9fd80
Deprecate service attribute key
michaelstaib Aug 30, 2026
a95f21a
Document keyed service constants
michaelstaib Aug 30, 2026
273af5b
Clarify derived service key documentation
michaelstaib Aug 30, 2026
764f5ce
Merge remote-tracking branch 'origin/main' into mst/dataloader-keyed-…
michaelstaib Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Collections.Immutable;
using HotChocolate.Types.Analyzers.Helpers;
using HotChocolate.Types.Analyzers.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace HotChocolate.Types.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DataLoaderKeyedServiceAttributeIgnoredAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[Errors.DataLoaderKeyedServiceAttributeIgnored];

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
}

private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var attributes = GenericDataLoaderAnalyzerHelper.GetDataLoaderAttributes(
context.SemanticModel,
methodDeclaration,
context.Compilation,
context.CancellationToken);

if (attributes.IsEmpty
|| context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken)
is not IMethodSymbol methodSymbol
|| methodSymbol.Parameters.IsEmpty)
{
return;
}

var parameters = DataLoaderInfo.CreateParameters(methodSymbol, context.Compilation);

foreach (var parameter in methodDeclaration.ParameterList.Parameters)
{
if (context.SemanticModel.GetDeclaredSymbol(parameter, context.CancellationToken)
is not IParameterSymbol parameterSymbol
|| parameters.FirstOrDefault(t => SymbolEqualityComparer.Default.Equals(
t.Parameter,
parameterSymbol)) is not { Kind: not DataLoaderParameterKind.Service })
{
continue;
}

foreach (var attributeList in parameter.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
if (IsKeyedServiceAttribute(
attribute,
parameterSymbol,
context.SemanticModel,
context.Compilation,
context.CancellationToken))
{
context.ReportDiagnostic(Diagnostic.Create(
Errors.DataLoaderKeyedServiceAttributeIgnored,
attribute.GetLocation()));
}
}
}
}
}

private static bool IsKeyedServiceAttribute(
AttributeSyntax attribute,
IParameterSymbol parameter,
SemanticModel semanticModel,
Compilation compilation,
CancellationToken cancellationToken)
{
if (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol is not IMethodSymbol attributeConstructor)
{
return false;
}

var attributeType = attributeConstructor.ContainingType;

if (attributeType.ToDisplayString() == WellKnownAttributes.FromKeyedServicesAttribute)
{
return true;
}

if (!attributeType.IsOrInheritsFrom(WellKnownAttributes.ServiceAttribute))
{
return false;
}

if (attributeType.ToDisplayString() == WellKnownAttributes.ServiceAttribute)
{
return attributeConstructor.Parameters.Length == 1;
}

var attributeData = parameter.GetAttributes().FirstOrDefault(t =>
t.ApplicationSyntaxReference is { } syntaxReference
&& syntaxReference.SyntaxTree == attribute.SyntaxTree
&& syntaxReference.Span == attribute.Span);

return attributeData?.GetDerivedServiceKey(compilation, out _)
== SymbolExtensions.ServiceKeyExtractionResult.Key;
}
Comment on lines +98 to +110
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace HotChocolate.Types.Analyzers;

[Shared]
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DataLoaderKeyedServiceAttributeIgnoredCodeFixProvider))]
public sealed class DataLoaderKeyedServiceAttributeIgnoredCodeFixProvider : CodeFixProvider
{
private const string Title = "Remove keyed service attribute";

public override ImmutableArray<string> FixableDiagnosticIds { get; } =
[ErrorCodes.Analyzers.DataLoaderKeyedServiceAttributeIgnored];

public override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

if (root is null)
{
return;
}

var attribute = root.FindNode(context.Diagnostics[0].Location.SourceSpan)
.AncestorsAndSelf()
.OfType<AttributeSyntax>()
.FirstOrDefault();

if (attribute is null)
{
return;
}

context.RegisterCodeFix(
CodeAction.Create(
Title,
c => RemoveAttributeAsync(context.Document, attribute, c),
Title),
context.Diagnostics);
}

private static async Task<Document> RemoveAttributeAsync(
Document document,
AttributeSyntax attribute,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

if (root is null || attribute.Parent is not AttributeListSyntax attributeList)
{
return document;
}

SyntaxNode? newRoot;

if (attributeList.Attributes.Count == 1)
{
newRoot = root.RemoveNode(attributeList, SyntaxRemoveOptions.KeepExteriorTrivia);
}
else
{
var newAttributeList = attributeList.RemoveNode(attribute, SyntaxRemoveOptions.KeepExteriorTrivia);
newRoot = newAttributeList is null ? null : root.ReplaceNode(attributeList, newAttributeList);
}

return newRoot is null ? document : document.WithSyntaxRoot(newRoot);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Collections.Immutable;
using HotChocolate.Types.Analyzers.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace HotChocolate.Types.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DataLoaderKeyedServiceConflictAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[Errors.DataLoaderKeyedServiceAttributeConflict];

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
}

private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var attributes = GenericDataLoaderAnalyzerHelper.GetDataLoaderAttributes(
context.SemanticModel,
methodDeclaration,
context.Compilation,
context.CancellationToken);

if (attributes.IsEmpty)
{
return;
}

foreach (var parameter in methodDeclaration.ParameterList.Parameters)
{
if (parameter.AttributeLists.Count == 0
|| context.SemanticModel.GetDeclaredSymbol(parameter, context.CancellationToken)
is not IParameterSymbol parameterSymbol)
{
continue;
}

var serviceAttributeInfo = parameterSymbol.GetServiceAttributeInfo(context.Compilation);

if (serviceAttributeInfo.HasServiceAttribute
&& serviceAttributeInfo.HasFromKeyedServicesAttribute)
{
context.ReportDiagnostic(Diagnostic.Create(
Errors.DataLoaderKeyedServiceAttributeConflict,
Location.Create(
parameter.SyntaxTree,
TextSpan.FromBounds(
parameter.AttributeLists[0].SpanStart,
parameter.AttributeLists[parameter.AttributeLists.Count - 1].Span.End))));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Collections.Immutable;
using HotChocolate.Types.Analyzers.Helpers;
using HotChocolate.Types.Analyzers.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace HotChocolate.Types.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DataLoaderKeyedServiceKeyNotDeterminableAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[Errors.DataLoaderKeyedServiceKeyNotDeterminable];

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
}

private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var attributes = GenericDataLoaderAnalyzerHelper.GetDataLoaderAttributes(
context.SemanticModel,
methodDeclaration,
context.Compilation,
context.CancellationToken);

if (attributes.IsEmpty
|| context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken)
is not IMethodSymbol methodSymbol
|| methodSymbol.Parameters.IsEmpty)
{
return;
}

var parameters = DataLoaderInfo.CreateParameters(methodSymbol, context.Compilation);

foreach (var parameter in parameters)
{
if (parameter.Kind is not DataLoaderParameterKind.Service)
{
continue;
}

var serviceAttributeInfo = parameter.Parameter.GetServiceAttributeInfo(context.Compilation);

if (!serviceAttributeInfo.IsServiceKeyUndeterminable)
{
continue;
}

foreach (var attribute in parameter.Parameter.GetAttributes())
{
if (attribute.AttributeClass is null
|| attribute.AttributeClass.ToDisplayString() == WellKnownAttributes.ServiceAttribute
|| !attribute.AttributeClass.IsOrInheritsFrom(WellKnownAttributes.ServiceAttribute)
|| attribute.GetDerivedServiceKey(context.Compilation, out _)
is not SymbolExtensions.ServiceKeyExtractionResult.Undeterminable)
{
continue;
}

var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)
.GetLocation()
?? parameter.Parameter.Locations.FirstOrDefault()
?? Location.None;

context.ReportDiagnostic(Diagnostic.Create(
Errors.DataLoaderKeyedServiceKeyNotDeterminable,
location));
}
}
}
}
Loading
Loading