Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -265,4 +265,13 @@ public static class Errors
category: "TypeSystem",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor LookupReturnsListType =
new(
id: "HC0114",
title: "Lookup Must Not Return List Type",
messageFormat: "A method or property with the [Lookup] attribute must not return a list type",
category: "TypeSystem",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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;

namespace HotChocolate.Types.Analyzers;

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

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration);
}
Comment thread
glen-84 marked this conversation as resolved.

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 = context.Compilation.IsTaskOrValueTask(methodSymbol.ReturnType, out var innerType)
? innerType
: methodSymbol.ReturnType;

if (!IsListType(returnType))
{
return;
}

var diagnostic = Diagnostic.Create(
Errors.LookupReturnsListType,
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 = context.Compilation.IsTaskOrValueTask(propertySymbol.Type, out var innerType)
? innerType
: propertySymbol.Type;

if (!IsListType(propertyType))
{
return;
}

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

context.ReportDiagnostic(diagnostic);
}

private static bool IsListType(ITypeSymbol typeSymbol)
=> typeSymbol is IArrayTypeSymbol || typeSymbol.IsListType(out _);

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
? innerType
: methodSymbol.ReturnType;

if (returnType is IArrayTypeSymbol || returnType.IsListType(out _))
{
return;
}

if (returnType.IsNullableType())
{
return;
Expand Down Expand Up @@ -71,6 +76,11 @@ private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context
? innerType
: propertySymbol.Type;

if (propertyType is IArrayTypeSymbol || propertyType.IsListType(out _))
{
return;
}

if (propertyType.IsNullableType())
{
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
namespace HotChocolate.Types;

public class LookupReturnsListTypeAnalyzerTests
{
[Fact]
public async Task Method_ListReturn_RaisesError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;
using System.Collections.Generic;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
[Lookup]
public static List<User?> GetUsersById(int id) => default!;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}

[Fact]
public async Task Method_ArrayReturn_RaisesError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
[Lookup]
public static User?[] GetUsersById(int id) => default!;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}

[Fact]
public async Task Method_IEnumerableReturn_RaisesError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;
using System.Collections.Generic;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
[Lookup]
public static IEnumerable<User?> GetUsersById(int id) => default!;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}

[Fact]
public async Task Method_TaskListReturn_RaisesError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
[Lookup]
public static Task<List<User?>> GetUsersByIdAsync(int id) => default!;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}

[Fact]
public async Task Property_ListReturn_RaisesError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;
using System.Collections.Generic;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
[Lookup]
public static List<User?> AllUsers => default!;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}

[Fact]
public async Task Method_SingleReturn_NoError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
[Lookup]
public static User? GetUserById(int id) => default;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}

[Fact]
public async Task Method_NoLookupAttribute_NoError()
{
await TestHelper.GetGeneratedSourceSnapshot(
["""
#nullable enable
using HotChocolate;
using HotChocolate.Types;
using HotChocolate.Types.Composite;
using System.Collections.Generic;

namespace TestNamespace;

[QueryType]
internal static partial class Query
{
public static List<User?> GetUsersById(int id) => default!;
}

public class User
{
public int Id { get; set; }
public string? Name { get; set; }
}
"""],
enableAnalyzers: true).MatchMarkdownAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ private static Snapshot CreateSnapshot(CSharpCompilation compilation, GeneratorD
new DataAttributeOrderAnalyzer(),
new IdAttributeOnRecordParameterAnalyzer(),
new WrongAuthorizationAttributeAnalyzer(),
new LookupReturnsNonNullableTypeAnalyzer());
new LookupReturnsNonNullableTypeAnalyzer(),
new LookupReturnsListTypeAnalyzer());

var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
var analyzerDiagnostics = compilationWithAnalyzers.GetAllDiagnosticsAsync().Result;
Expand Down
Loading
Loading