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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,6 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml

# Snapshot tests
*.received.txt
6 changes: 3 additions & 3 deletions src/QueryByShape.Analyzer/Analyzers/ArgumentAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
var attributes = context.ContainingSymbol!.GetAttributes();
var activeAttribute = attributes.Single(a => a.ApplicationSyntaxReference?.SyntaxTree == attributeSyntax.SyntaxTree && a.ApplicationSyntaxReference?.Span == attributeSyntax.Span);

if (activeAttribute.AttributeClass?.Equals(attributeNamedType, SymbolEqualityComparer.Default) != true)
if (activeAttribute.IsAttributeType(attributeNamedType) is false
|| activeAttribute.TryGetConstructorArgument(out string? activeName) is false)
{
return;
}

var activeName = activeAttribute.GetConstructorArgument();
ReportInvalidName(activeName, context);
ReportDuplicateNames(activeName, activeAttribute, attributeNamedType, attributes, context);

Expand All @@ -63,7 +63,7 @@ private static void ReportInvalidName(string name, SyntaxNodeAnalysisContext con

private static void ReportDuplicateNames(string name, AttributeData activeAttribute, INamedTypeSymbol attributeNamedType, ImmutableArray<AttributeData> attributes, SyntaxNodeAnalysisContext context)
{
var dupes = attributes.Where(a => a.AttributeClass?.Equals(attributeNamedType, SymbolEqualityComparer.Default) == true && a.GetConstructorArgument() == name);
var dupes = attributes.Where(a => a.IsAttributeType(attributeNamedType) && a.TryGetConstructorArgument(out string? argument) && argument == name);

if (dupes.First() != activeAttribute)
{
Expand Down
6 changes: 3 additions & 3 deletions src/QueryByShape.Analyzer/Analyzers/VariableAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
var attributes = context.ContainingSymbol!.GetAttributes();
var activeAttribute = attributes.Single(a => a.ApplicationSyntaxReference?.SyntaxTree == attributeSyntax.SyntaxTree && a.ApplicationSyntaxReference?.Span == attributeSyntax.Span);

if (activeAttribute.AttributeClass?.Equals(attributeNamedType, SymbolEqualityComparer.Default) != true)
if (activeAttribute.IsAttributeType(attributeNamedType) is false
|| activeAttribute.TryGetConstructorArgument(out string? activeName) is false)
{
return;
}

var activeName = activeAttribute.GetConstructorArgument();
ReportInvalidName(activeName, context);
ReportDuplicateNames(activeName, activeAttribute, attributeNamedType, attributes, context);
}
Expand Down Expand Up @@ -73,7 +73,7 @@ private static void ReportInvalidName(string name, SyntaxNodeAnalysisContext con

private static void ReportDuplicateNames(string name, AttributeData activeAttribute, INamedTypeSymbol attributeNamedType, ImmutableArray<AttributeData> attributes, SyntaxNodeAnalysisContext context)
{
var dupes = attributes.Where(a => a.AttributeClass?.Equals(attributeNamedType, SymbolEqualityComparer.Default) == true && a.GetConstructorArgument() == name);
var dupes = attributes.Where(a => a.IsAttributeType(attributeNamedType) && a.TryGetConstructorArgument(out string? argument) && argument == name);

if (dupes.First() != activeAttribute)
{
Expand Down
1 change: 1 addition & 0 deletions src/QueryByShape.Analyzer/AttributeNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal static class AttributeNames
public const string JSON_PROPERTY = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
public const string JSON_IGNORE = "System.Text.Json.Serialization.JsonIgnoreAttribute";
public const string QUERY = $"QueryByShape.{nameof(QueryAttribute)}";
public const string MUTATION = $"QueryByShape.{nameof(MutationAttribute)}";
public const string VARIABLE = $"QueryByShape.{nameof(VariableAttribute)}";
public const string ARGUMENT = $"QueryByShape.{nameof(ArgumentAttribute)}";
public const string ALIAS_OF = $"QueryByShape.{nameof(AliasOfAttribute)}";
Expand Down
87 changes: 63 additions & 24 deletions src/QueryByShape.Analyzer/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using System.Xml.Linq;
Expand All @@ -12,8 +13,10 @@ internal static class Extensions
{
public static string ToFullName(this AttributeData attribute)
{
var attributeClass = attribute.AttributeClass ?? throw new NullReferenceException();
return $"{attributeClass.GetNamespace()}.{attributeClass.Name}";
return attribute.AttributeClass?
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", string.Empty)
?? throw new ArgumentNullException(nameof(attribute));
}

public static string ExtractName(this NameSyntax nameSyntax)
Expand Down Expand Up @@ -47,42 +50,69 @@ public static Location GetLocation(this AttributeData attribute)

public static INamedTypeSymbol ResolveNamedType<T>(this Compilation compilation)
{
return compilation.GetTypeByMetadataName(typeof(T).FullName)!;
var name = typeof(T).FullName ?? throw new InvalidOperationException("No metadata name for type");
var symbol = compilation.GetTypeByMetadataName(name);
return symbol ?? throw new InvalidOperationException($"Type '{name}' not found in compilation");
}

public static string GetConstructorArgument(this AttributeData attribute) => GetConstructorArguments(attribute)[0];
public static string[] GetConstructorArguments(this AttributeData attribute) => attribute.ConstructorArguments.Select(c => c.Value?.ToString() ?? "").ToArray();

public static bool TryGetNamedArgument<T>(this AttributeData attribute, string name, out T? value)
{
public static bool TryGetConstructorArgument<T>(this AttributeData attribute, [NotNullWhen(true)] out T? value)
{
if (attribute?.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is T argument)
{
value = argument;
return true;
}

value = default;
return false;
}

var arguments = attribute.NamedArguments.Where(a => a.Key == name);
if (arguments.Any())
public static bool TryGetConstructorArguments<TFirst, TSecond>(this AttributeData attribute, [NotNullWhen(true)] out TFirst? first, [NotNullWhen(true)] out TSecond? second)
{
if (attribute?.ConstructorArguments.Length > 1 && attribute.ConstructorArguments[0].Value is TFirst firstArg && attribute.ConstructorArguments[1].Value is TSecond secondArg)
{
value = (T?)arguments.First().Value.Value;
first = firstArg;
second = secondArg;
return true;
}

first = default;
second = default;
return false;
}

public static string GetNamespace(this INamedTypeSymbol symbol)
public static bool TryGetNamedArgument<T>(this AttributeData attribute, string name, out T? value)
{
return string.Join(".", GetNamespace_Internal(symbol.ContainingNamespace));
foreach (var kv in attribute.NamedArguments)
{
if (kv.Key == name && kv.Value.Value is T t)
{
value = t;
return true;
}
}

static string[] GetNamespace_Internal(INamespaceSymbol symbol, int index = 0)
value = default;
return false;
}

public static bool IsAttributeType(this AttributeData? attribute, INamedTypeSymbol? targetType)
{
var sourceType = attribute?.AttributeClass;

if (sourceType is null || targetType is null)
{
if (symbol.ContainingNamespace == null)
{
return new string[index];
}

var result = GetNamespace_Internal(symbol.ContainingNamespace, index + 1);
result[result.Length - index - 1] = symbol.Name;
return result;
return false;
}

return SymbolEqualityComparer.Default.Equals(sourceType, targetType);
}

public static string GetNamespace(this INamedTypeSymbol symbol)
{
return symbol.ContainingNamespace
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", string.Empty);
}

public static bool TryGetCompatibleGenericBaseType(this ITypeSymbol type, INamedTypeSymbol? baseType, out INamedTypeSymbol? result)
Expand Down Expand Up @@ -150,6 +180,15 @@ public static bool IsAssignableFrom(this ITypeSymbol? baseType, ITypeSymbol? typ
return false;
}

public static Location ToTrimmedLocation(this Location location) => Location.Create(location.SourceTree!.FilePath, location.SourceSpan, location.GetLineSpan().Span);
public static Location ToTrimmedLocation(this Location location)
{
if (location == Location.None || location.SourceTree is null)
{
return location;
}

return Location.Create(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span);
}

}
}
54 changes: 19 additions & 35 deletions src/QueryByShape.Analyzer/Emitter/QueryEmitter.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using QueryByShape.Analyzer.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

Expand All @@ -28,14 +26,13 @@ public static void EmitSource(SourceProductionContext ctx, ParseResult result)
}
private string EmitQuery()
{
// variables dynamic or static set? use case
// variables with object type (sorting)

_sb.Append("query ");
_sb.Append(FormatName(query.Name ?? query.TypeName));

EmitParameters(query.Variables?.Select(v => v.DefaultValue == null ? $"{v.Name}:{v.GraphType}" : $"{v.Name}:{v.GraphType} = {JsonSerializer.Serialize(v.DefaultValue)}"));
EmitType(query.Type!, query.Options, 1);
var variables = query.Variables?.Select(v => v.DefaultValue == null ? $"{v.Name}:{v.GraphType}" : $"{v.Name}:{v.GraphType} = {JsonSerializer.Serialize(v.DefaultValue)}").ToArray();
_sb.AppendParentheses(variables);

EmitType(query.Type, query.Options);

return _sb.ToString();
}
Expand All @@ -50,7 +47,7 @@ private string FormatName(string name)
return name;
}

private void EmitType(TypeMetadata typeMetadata, QueryOptions options, int depth)
private void EmitType(TypeMetadata typeMetadata, QueryOptions options)
{
if (typeMetadata.Members is null)
{
Expand All @@ -59,9 +56,9 @@ private void EmitType(TypeMetadata typeMetadata, QueryOptions options, int depth

var (members, fragmentMembers) = typeMetadata.Members.Value.Partition(m => m.On?.Count is not > 0);

_sb.AppendLine("{");
_sb.AppendStartBlock();

EmitMembers(members, options, depth);
EmitMembers(members, options);

if (fragmentMembers.Count > 0)
{
Expand All @@ -71,22 +68,24 @@ private void EmitType(TypeMetadata typeMetadata, QueryOptions options, int depth

foreach (var fragment in fragments)
{
_sb.AppendLine($"... on {fragment.Key} {{", depth);
EmitMembers(fragment, options, depth + 1);
_sb.AppendLine("}", depth);
_sb.AppendLine();
_sb.Append($"... on {fragment.Key}");
_sb.AppendStartBlock();
EmitMembers(fragment, options);
_sb.AppendEndBlock();
}
}

_sb.AppendLine("}", depth - 1);
_sb.AppendEndBlock();
}

private void EmitMembers(IEnumerable<MemberMetadata> members, QueryOptions options, int depth)
private void EmitMembers(IEnumerable<MemberMetadata> members, QueryOptions options)
{
var filtered = options.IncludeFields == false ? members.Where(m => m.Kind == SymbolKind.Property) : members;

foreach (var member in filtered)
{
_sb.AppendIndent(depth);
_sb.AppendLine();
_sb.Append(member.OverrideName ?? FormatName(member.Name));

if (member.AliasOf != null)
Expand All @@ -95,29 +94,14 @@ private void EmitMembers(IEnumerable<MemberMetadata> members, QueryOptions optio
_sb.Append(member.AliasOf);
}

EmitParameters(member.Arguments?.Select(a => $"{a.Name}:{a.VariableName}"));

var arguments = member.Arguments?.Select(a => $"{a.Name}:{a.VariableName}").ToArray();
_sb.AppendParentheses(arguments);

if (member.ChildrenType?.Members?.Count > 0)
{
EmitType(member.ChildrenType, options, depth + 1);
EmitType(member.ChildrenType, options);
}
else
{
_sb.AppendLine(string.Empty);
}
}
}

internal void EmitParameters(IEnumerable<string>? items)
{
if (items == null || !items.Any())
{
return;
}

_sb.Append("(");
_sb.Append(string.Join(",", items));
_sb.Append(")");
}
}
}
52 changes: 40 additions & 12 deletions src/QueryByShape.Analyzer/Emitter/SourceBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,70 @@
using System.Text;
using System;
using System.Text;
using static System.Net.Mime.MediaTypeNames;

namespace QueryByShape.Analyzer
{
internal class SourceBuilder(SourceFormatting formatting)
{
private StringBuilder sb = new StringBuilder();

public void Append(string text)
private readonly StringBuilder sb = new StringBuilder();
private int depth = 0;

internal void AppendStartBlock()
{
sb.Append(text);
sb.Append(" {");
depth++;
}

public void AppendLine(string text, int depth = 0)
internal void AppendEndBlock()
{
depth--;
AppendLine();
sb.Append("}");
}

internal void AppendLine()
{
AppendIndent(depth);

if (formatting == SourceFormatting.Minified)
{
sb.Append(text);
sb.Append(" ");
}
else
{
sb.AppendLine(text);
sb.AppendLine();
AppendIndent();
}
}

public void AppendIndent(int depth)
internal void AppendIndent()
{
if (formatting == SourceFormatting.Minified)
{
sb.Append(" ");
}
else if (depth > 0)
{
sb.Append(' ', depth * 4);
sb.Append(' ', depth * 4);
}
}

internal void Append(string text)
{
sb.Append(text);
}

internal void AppendParentheses(string[]? values)
{
if (values == null || values.Length == 0)
{
return;
}

sb.Append("(");
sb.AppendJoin(",", values);
sb.Append(")");
}

public override string ToString() => sb.ToString();
}

}
Loading