Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
9 changes: 9 additions & 0 deletions src/HotChocolate/Core/src/Types.Analyzers/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,13 @@ public static class Errors
category: "TypeSystem",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LookupReturnsNonNullableType =
new(
id: "HC0113",
title: "Lookup Must Return Nullable Type",
messageFormat: "A method or property with the [Lookup] attribute must return a nullable type",
category: "TypeSystem",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System.Collections.Immutable;
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 LookupReturnsNonNullableTypeAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[Errors.LookupReturnsNonNullableType];

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

private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;

if (!HasLookupAttribute(context, methodDeclaration.AttributeLists))
{
return;
}

var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration);
if (methodSymbol is null)
{
return;
}

var returnType = UnwrapTaskType(methodSymbol.ReturnType);

if (IsNullableType(returnType))
{
return;
}

var diagnostic = Diagnostic.Create(
Errors.LookupReturnsNonNullableType,
methodDeclaration.ReturnType.GetLocation());

context.ReportDiagnostic(diagnostic);
}

private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context)
{
var propertyDeclaration = (PropertyDeclarationSyntax)context.Node;

if (!HasLookupAttribute(context, propertyDeclaration.AttributeLists))
{
return;
}

var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
if (propertySymbol is null)
{
return;
}

var propertyType = UnwrapTaskType(propertySymbol.Type);

if (IsNullableType(propertyType))
{
return;
}

var diagnostic = Diagnostic.Create(
Errors.LookupReturnsNonNullableType,
propertyDeclaration.Type.GetLocation());

context.ReportDiagnostic(diagnostic);
}

private static bool HasLookupAttribute(
SyntaxNodeAnalysisContext context,
SyntaxList<AttributeListSyntax> attributeLists)
{
var semanticModel = context.SemanticModel;

foreach (var attributeList in attributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var symbolInfo = semanticModel.GetSymbolInfo(attribute);
if (symbolInfo.Symbol is not IMethodSymbol attributeSymbol)
{
continue;
}

var attributeType = attributeSymbol.ContainingType;
if (attributeType.ToDisplayString() == WellKnownAttributes.LookupAttribute)
{
return true;
}
}
}

return false;
}

private static ITypeSymbol UnwrapTaskType(ITypeSymbol typeSymbol)
{
if (typeSymbol is INamedTypeSymbol namedType
&& namedType.TypeArguments.Length == 1
&& namedType.Name is nameof(Task) or nameof(ValueTask))
{
return namedType.TypeArguments[0];
}

return typeSymbol;
}

private static bool IsNullableType(ITypeSymbol typeSymbol)
{
if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated)
{
return true;
}

if (typeSymbol is INamedTypeSymbol namedType
&& namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
return true;
}

return false;
}
Comment thread
glen-84 marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace HotChocolate.Types.Analyzers;

[Shared]
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LookupReturnsNonNullableTypeCodeFixProvider))]
public sealed class LookupReturnsNonNullableTypeCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ["HC0113"];

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 diagnostic = context.Diagnostics[0];
var diagnosticSpan = diagnostic.Location.SourceSpan;

var node = root.FindNode(diagnosticSpan);

// Determine the type syntax to make nullable.
TypeSyntax? typeSyntax = null;

var methodDeclaration = node.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().FirstOrDefault();
if (methodDeclaration is not null)
{
typeSyntax = methodDeclaration.ReturnType;
}
else
{
var propertyDeclaration = node.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
if (propertyDeclaration is not null)
{
typeSyntax = propertyDeclaration.Type;
}
}

if (typeSyntax is null)
{
return;
}

const string title = "Make return type nullable";

context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => MakeReturnTypeNullableAsync(
context.Document,
typeSyntax,
c),
equivalenceKey: title),
diagnostic);
}

private static async Task<Document> MakeReturnTypeNullableAsync(
Document document,
TypeSyntax typeSyntax,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is null)
{
return document;
}

TypeSyntax newTypeSyntax;

// If the type is Task<T> or ValueTask<T>, wrap the inner type argument.
if (typeSyntax is GenericNameSyntax genericName
&& genericName.TypeArgumentList.Arguments.Count == 1
&& genericName.Identifier.Text is nameof(Task) or nameof(ValueTask))
{
var innerType = genericName.TypeArgumentList.Arguments[0];

// Guard against double-wrapping.
if (innerType is NullableTypeSyntax)
{
return document;
}

var nullableInnerType = SyntaxFactory.NullableType(innerType);
var newTypeArgumentList = genericName.TypeArgumentList.WithArguments(
SyntaxFactory.SingletonSeparatedList<TypeSyntax>(nullableInnerType));
newTypeSyntax = genericName.WithTypeArgumentList(newTypeArgumentList);
}
Comment thread
glen-84 marked this conversation as resolved.
Outdated
else
{
// Guard against double-wrapping.
if (typeSyntax is NullableTypeSyntax)
{
return document;
}

newTypeSyntax = SyntaxFactory.NullableType(typeSyntax);
}
Comment thread
glen-84 marked this conversation as resolved.

newTypeSyntax = newTypeSyntax.WithTriviaFrom(typeSyntax);
var newRoot = root.ReplaceNode(typeSyntax, newTypeSyntax);
return document.WithSyntaxRoot(newRoot);
}
}
Loading
Loading