diff --git a/QueryByShape.sln b/QueryByShape.sln index 0851e98..be2fde2 100644 --- a/QueryByShape.sln +++ b/QueryByShape.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32516.85 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11201.2 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryByShape", "src\QueryByShape\QueryByShape.csproj", "{0B6A344B-FCCE-428D-A2A7-AF1409A86C7F}" EndProject @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{6D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarWars", "examples\StarWars\StarWars.csproj", "{E26A1C68-EC4D-4A3C-9EEF-7C1DBCA8AC1F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryByShape.Analyzer.Benchmark", "tests\QueryByShape.Analyzer.Benchmark\QueryByShape.Analyzer.Benchmark.csproj", "{49638B8D-2B15-4AE7-ABA0-B296645D631C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,6 +51,10 @@ Global {E26A1C68-EC4D-4A3C-9EEF-7C1DBCA8AC1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {E26A1C68-EC4D-4A3C-9EEF-7C1DBCA8AC1F}.Release|Any CPU.ActiveCfg = Release|Any CPU {E26A1C68-EC4D-4A3C-9EEF-7C1DBCA8AC1F}.Release|Any CPU.Build.0 = Release|Any CPU + {49638B8D-2B15-4AE7-ABA0-B296645D631C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49638B8D-2B15-4AE7-ABA0-B296645D631C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49638B8D-2B15-4AE7-ABA0-B296645D631C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49638B8D-2B15-4AE7-ABA0-B296645D631C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -56,6 +62,7 @@ Global GlobalSection(NestedProjects) = preSolution {70312BAD-B5F9-4FFA-86DF-A4B38B830C7C} = {91D9FD32-6EC7-4E31-978F-124B427C126B} {E26A1C68-EC4D-4A3C-9EEF-7C1DBCA8AC1F} = {6D432921-65BB-40FE-856A-C087B5ACC0A2} + {49638B8D-2B15-4AE7-ABA0-B296645D631C} = {91D9FD32-6EC7-4E31-978F-124B427C126B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2182D04A-84B1-4FB0-BF83-FD2D148552DF} diff --git a/src/QueryByShape.Analyzer/ArrayBuilder.cs b/src/QueryByShape.Analyzer/ArrayBuilder.cs new file mode 100644 index 0000000..3719894 --- /dev/null +++ b/src/QueryByShape.Analyzer/ArrayBuilder.cs @@ -0,0 +1,63 @@ +using System; + +namespace QueryByShape.Analyzer +{ + internal class ArrayBuilder + { + private const int DefaultCapacity = 8; + private T[] _items; + + public ArrayBuilder(int capacity = DefaultCapacity) + { + _items = new T[capacity]; + } + + public int Count { get; set; } + + private void EnsureCapacity(int additional) + { + int requiredCapacity = this.Count + additional; + if (requiredCapacity <= _items.Length) + { + return; + } + + int newCapacity = Math.Max(_items.Length * 2, requiredCapacity); + Array.Resize(ref _items, newCapacity); + } + + public void Add(T item) + { + EnsureCapacity(1); + _items[this.Count++] = item; + } + + public void AddRange(T[] items) + { + EnsureCapacity(items.Length); + + int offset = this.Count; + this.Count += items.Length; + + Array.Copy(items, 0, _items, offset, items.Length); + } + + + public T[] ToArray() + { + if (this.Count == 0) + { + return Array.Empty(); + } + + T[] result = new T[this.Count]; + Array.Copy(_items, result, this.Count); + return result; + } + + public void Clear() + { + this.Count = 0; + } + } +} diff --git a/src/QueryByShape.Analyzer/Emitter/GeneratorTemplate.cs b/src/QueryByShape.Analyzer/Emitter/GeneratorTemplate.cs index 503eae1..ad8b22e 100644 --- a/src/QueryByShape.Analyzer/Emitter/GeneratorTemplate.cs +++ b/src/QueryByShape.Analyzer/Emitter/GeneratorTemplate.cs @@ -6,11 +6,11 @@ internal static class GeneratorTemplate { public static string Build(string query, string namespaceName, string className) { - return string.Format(Output, DateTime.Now.ToLongTimeString(), namespaceName, className, query); + return string.Format(Output, namespaceName, className, query); } public static string Output = """ -// {0} +// #nullable enable annotations #nullable disable warnings @@ -18,11 +18,11 @@ public static string Build(string query, string namespaceName, string className) // Suppress warnings about [Obsolete] member usage in generated code. // #pragma warning disable CS0612, CS0618 -namespace {1} +namespace {0} {{ - public partial class {2} + public partial class {1} {{ - public static string ToGraphQLQuery() => @"{3}"; + public static string ToGraphQLQuery() => @"{2}"; }} }} """; diff --git a/src/QueryByShape.Analyzer/Emitter/QueryEmitter.cs b/src/QueryByShape.Analyzer/Emitter/QueryEmitter.cs index b17f140..ab140f7 100644 --- a/src/QueryByShape.Analyzer/Emitter/QueryEmitter.cs +++ b/src/QueryByShape.Analyzer/Emitter/QueryEmitter.cs @@ -17,23 +17,50 @@ public static void EmitSource(SourceProductionContext ctx, ParseResult result) var (query, diagnostics) = result; var emitter = new QueryEmitter(query); - if (diagnostics.Any(d => d.Descriptor.DefaultSeverity is DiagnosticSeverity.Error) is false) + if (diagnostics?.Any(d => d.Descriptor.DefaultSeverity is DiagnosticSeverity.Error) == true) { - string graphQlQuery = emitter.EmitQuery(); - var generatedClass = GeneratorTemplate.Build(graphQlQuery, query.NamespaceName, query.TypeName); - ctx.AddSource($"QueryByShape.{query.TypeFullName}.Query.g.cs", SourceText.From(generatedClass, Encoding.UTF8)); + return; } + + string graphQlQuery = emitter.EmitQuery(); + var generatedClass = GeneratorTemplate.Build(graphQlQuery, query.NamespaceName, query.TypeName); + ctx.AddSource($"QueryByShape.{query.TypeFullName}.Query.g.cs", SourceText.From(generatedClass, Encoding.UTF8)); } private string EmitQuery() { _sb.Append("query "); _sb.Append(FormatName(query.Name ?? query.TypeName)); - 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); - + if (query.Variables?.Count > 0) + { + var variables = query.Variables.Value; + _sb.Append("("); + + for (int i = 0; i < variables.Count; i++) + { + var variable = variables[i]; + + if (i != 0) + { + _sb.Append(","); + } + + _sb.Append(variable.Name); + _sb.Append(":"); + _sb.Append(variable.GraphType); + + if (variable.DefaultValue != null) + { + _sb.Append(" = "); + _sb.Append(JsonSerializer.Serialize(variable.DefaultValue)); + } + } + + _sb.Append(")"); + } + EmitType(query.Type, query.Options); - + return _sb.ToString(); } @@ -49,29 +76,49 @@ private string FormatName(string name) private void EmitType(TypeMetadata typeMetadata, QueryOptions options) { - if (typeMetadata.Members is null) + var members = typeMetadata.Members; + + var withoutFragments = new List(members.Count); + var withFragments = new Dictionary>(); + + foreach (var member in typeMetadata.Members) { - return; - } + if (member.Ignore == true || (member.Kind == SymbolKind.Field && !options.IncludeFields)) + { + continue; + } - var (members, fragmentMembers) = typeMetadata.Members.Value.Partition(m => m.On?.Count is not > 0); + if (member.On?.Count > 0) + { + foreach (var fragment in member.On) + { + if (!withFragments.TryGetValue(fragment, out var fragmentMembers)) + { + fragmentMembers = new List(); + withFragments[fragment] = fragmentMembers; + } + + fragmentMembers.Add(member); + } + } + else + { + withoutFragments.Add(member); + } + } _sb.AppendStartBlock(); - EmitMembers(members, options); + EmitMembers(withoutFragments, options); - if (fragmentMembers.Count > 0) + if (withFragments.Count > 0) { - var fragments = fragmentMembers - .SelectMany(m => m.On!.Value, (m, o) => (fragment: o, member: m)) - .GroupBy(m => m.fragment, m => m.member); - - foreach (var fragment in fragments) + foreach (var fragment in withFragments) { _sb.AppendLine(); _sb.Append($"... on {fragment.Key}"); _sb.AppendStartBlock(); - EmitMembers(fragment, options); + EmitMembers(fragment.Value, options); _sb.AppendEndBlock(); } } @@ -79,11 +126,9 @@ private void EmitType(TypeMetadata typeMetadata, QueryOptions options) _sb.AppendEndBlock(); } - private void EmitMembers(IEnumerable members, QueryOptions options) + private void EmitMembers(List members, QueryOptions options) { - var filtered = options.IncludeFields == false ? members.Where(m => m.Kind == SymbolKind.Property) : members; - - foreach (var member in filtered) + foreach (var member in members) { _sb.AppendLine(); _sb.Append(member.OverrideName ?? FormatName(member.Name)); @@ -94,10 +139,29 @@ private void EmitMembers(IEnumerable members, QueryOptions optio _sb.Append(member.AliasOf); } - var arguments = member.Arguments?.Select(a => $"{a.Name}:{a.VariableName}").ToArray(); - _sb.AppendParentheses(arguments); + if (member.Arguments?.Count > 0) + { + var arguments = member.Arguments.Value; + _sb.Append("("); + + for (int i = 0; i < arguments.Count; i++) + { + var argument = arguments[i]; + + if (i != 0) + { + _sb.Append(","); + } + + _sb.Append(argument.Name); + _sb.Append(":"); + _sb.Append(argument.VariableName); + } + + _sb.Append(")"); + } - if (member.ChildrenType?.Members?.Count > 0) + if (member.ChildrenType?.Members.Count > 0) { EmitType(member.ChildrenType, options); } diff --git a/src/QueryByShape.Analyzer/Emitter/SourceBuilder.cs b/src/QueryByShape.Analyzer/Emitter/SourceBuilder.cs index 2ec6815..f3cf631 100644 --- a/src/QueryByShape.Analyzer/Emitter/SourceBuilder.cs +++ b/src/QueryByShape.Analyzer/Emitter/SourceBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using static Microsoft.CodeAnalysis.CSharp.SyntaxTokenParser; using static System.Net.Mime.MediaTypeNames; namespace QueryByShape.Analyzer @@ -52,18 +53,6 @@ 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(); } diff --git a/src/QueryByShape.Analyzer/EnumerableExtensions.cs b/src/QueryByShape.Analyzer/EnumerableExtensions.cs deleted file mode 100644 index 8923233..0000000 --- a/src/QueryByShape.Analyzer/EnumerableExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace QueryByShape.Analyzer -{ - public static class EnumerableExtensions - { - public static (List, List) Partition(this IEnumerable source, Func predicate) - { - var left = new List(); - var right = new List(); - - foreach (var item in source) - { - var result = predicate(item); - - if (predicate(item) is true) - { - left.Add(item); - } - else - { - right.Add(item); - } - } - - return (left, right); - } - } -} diff --git a/src/QueryByShape.Analyzer/EquatableArray.cs b/src/QueryByShape.Analyzer/EquatableArray.cs index 3ab4c10..07a004a 100644 --- a/src/QueryByShape.Analyzer/EquatableArray.cs +++ b/src/QueryByShape.Analyzer/EquatableArray.cs @@ -1,8 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; +using System.Xml.Linq; namespace QueryByShape.Analyzer { @@ -16,35 +20,23 @@ internal static class EquatableArrayBuilder /// /// The input to wrap. [CollectionBuilder(typeof(EquatableArrayBuilder), nameof(EquatableArrayBuilder.Create))] - internal readonly struct EquatableArray(T[] array) : IEquatable>, IEnumerable where T : IEquatable + internal readonly struct EquatableArray(T[] array) : IList, IEquatable> where T : IEquatable { - public static readonly EquatableArray Empty = new([]); - - /// - /// The underlying array. - /// - private readonly T[]? _array = array; - /// - public bool Equals(EquatableArray array) + public bool Equals(EquatableArray compare) { - return AsSpan().SequenceEqual(array.AsSpan()); + return AsSpan().SequenceEqual(compare.AsSpan()); } /// public override bool Equals(object? obj) { - return obj is EquatableArray array && Equals(this, array); + return obj is EquatableArray compare && Equals(this, compare); } /// public override int GetHashCode() { - if (_array is not T[] array) - { - return 0; - } - HashCode hashCode = default; foreach (T item in array) @@ -61,27 +53,59 @@ public override int GetHashCode() /// A wrapping the current items. public ReadOnlySpan AsSpan() { - return _array.AsSpan(); + return array.AsSpan(); } - /// - /// Gets the underlying array if there is one - /// - public T[]? GetArray() => _array; - /// IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)(_array ?? [])).GetEnumerator(); + return ((IEnumerable)(array)).GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)(_array ?? [])).GetEnumerator(); + return ((IEnumerable)(array)).GetEnumerator(); + } + + public void Add(T item) + { + throw new NotSupportedException(); + } + + public void Clear() + { + throw new NotSupportedException(); } - public int Count => _array?.Length ?? 0; + public bool Contains(T item) => array.Contains(item); + + public void CopyTo(T[] destination, int arrayIndex) => array.CopyTo(destination, arrayIndex); + + public bool Remove(T item) + { + throw new NotSupportedException(); + } + + public int IndexOf(T item) => Array.IndexOf(array, item); + + public void Insert(int index, T item) + { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } + + public int Count => array.Length; + + public bool IsReadOnly => true; + + T IList.this[int index] { get => array[index]; set => throw new NotSupportedException(); } + + public T this[int index] => array[index]; public static bool operator ==(EquatableArray left, EquatableArray right) { diff --git a/src/QueryByShape.Analyzer/NamedTypeSymbols.cs b/src/QueryByShape.Analyzer/NamedTypeSymbols.cs index 3d4348d..31f9bf4 100644 --- a/src/QueryByShape.Analyzer/NamedTypeSymbols.cs +++ b/src/QueryByShape.Analyzer/NamedTypeSymbols.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json.Serialization; @@ -84,7 +85,7 @@ public bool IsTypeSerializable(INamedTypeSymbol type) ) is false; } - public bool TryGetChildrenType(ITypeSymbol type, out INamedTypeSymbol? childrenType) + public bool TryGetChildrenType(ITypeSymbol type, [NotNullWhen(true)] out INamedTypeSymbol? childrenType) { childrenType = null; ITypeSymbol? effectiveType = null; diff --git a/src/QueryByShape.Analyzer/Parser/QueryParser.cs b/src/QueryByShape.Analyzer/Parser/QueryParser.cs index 4c0df8c..aa4e5b9 100644 --- a/src/QueryByShape.Analyzer/Parser/QueryParser.cs +++ b/src/QueryByShape.Analyzer/Parser/QueryParser.cs @@ -1,4 +1,4 @@ -global using ParseResult = (QueryByShape.Analyzer.QueryMetadata Metadata, QueryByShape.Analyzer.EquatableArray Diagnostics); +global using ParseResult = (QueryByShape.Analyzer.QueryMetadata Metadata, QueryByShape.Analyzer.EquatableArray? Diagnostics); using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -6,80 +6,52 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.ComponentModel; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Runtime.InteropServices; +using System.Reflection; using System.Text.Json.Serialization; using System.Threading; -using System.Xml.Linq; namespace QueryByShape.Analyzer { internal class QueryParser(NamedTypeSymbols symbols) { - private static readonly SymbolEqualityComparer _symbolComparer = SymbolEqualityComparer.Default; private static readonly StringComparer _stringComparer = StringComparer.Ordinal; - private readonly List _diagnostics = new(); - private readonly Dictionary)> _typeCache = - new(_symbolComparer); - - public static EquatableArray Process(ImmutableArray<(TypeDeclarationSyntax declaration, SemanticModel semanticModel)> contexts, NamedTypeSymbols symbols, CancellationToken cancellationToken) - { - var items = new ParseResult[contexts.Length]; - var parser = new QueryParser(symbols); - - for (int i = 0; i < contexts.Length; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var (declaration, semanticModel) = contexts[i]; - var declaredSymbol = semanticModel.GetDeclaredSymbol(declaration)!; - - items[i] = parser.Parse(declaration, declaredSymbol); - } - - return [.. items]; - } - - public ParseResult Parse(TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol declaredSymbol) + public ParseResult Parse(TypeDeclarationSyntax typeDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken) { - if (typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) is false) - { - //_diagnostics.Add(QueryMustBePartialDiagnostic.Create(declaredSymbol.Name, typeDeclaration.GetLocation())); - } + var declaredSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration)!; - var (types, queryArguments) = ParseTypeMetadata(declaredSymbol); + var (types, queryArguments) = ParseTypeMetadata(declaredSymbol, cancellationToken); var query = new QueryMetadata(declaredSymbol.Name, declaredSymbol.GetNamespace(), types); - + UpdateQueryFromAttributes(query, declaredSymbol.GetAttributes()); - ValidateVariables(query.Variables, queryArguments); + ValidateVariables(query.Variables, queryArguments, out var diagnostics); - return (query, [.. _diagnostics]); + return (query, diagnostics != null ? [.. diagnostics] : null); } - public void ValidateVariables(EquatableArray? variables, IList arguments) + public void ValidateVariables(EquatableArray? variables, IList arguments, out List? diagnostics) { + diagnostics = null; + // Nothing to validate - if ((variables is null or { Count: 0 }) && arguments is { Count: 0 }) + if ((variables == null || variables?.Count == 0) && arguments.Count == 0) { return; } - var vars = variables?.GetArray() ?? Array.Empty(); - - var variableRefs = new ReferenceSet(vars, v => v.Name, StringComparer.Ordinal); + var variableRefs = new ReferenceSet(variables, v => v.Name, StringComparer.Ordinal); foreach (var argument in arguments) { var varName = argument.VariableName; - if (variableRefs.TryMarkReferenced(varName) is false) + if (!variableRefs.TryMarkReferenced(varName)) { - _diagnostics.Add(MissingVariableDiagnostic.CreateMetadata(argument.Name, varName, argument.Reference.GetLocation())); + diagnostics ??= new List(); + diagnostics.Add(MissingVariableDiagnostic.CreateMetadata(argument.Name, varName, argument.Reference.GetLocation())); continue; } } @@ -87,14 +59,17 @@ public void ValidateVariables(EquatableArray? variables, IList // Any remaining variables were not used by any argument foreach (var variable in variableRefs.GetUnreferenced()) { - _diagnostics.Add(UnusedVariableDiagnostic.CreateMetadata(variable.Name, variable.Reference.GetLocation())); + diagnostics ??= new List(); + diagnostics.Add(UnusedVariableDiagnostic.CreateMetadata(variable.Name, variable.Reference.GetLocation())); } + } private void UpdateQueryFromAttributes(QueryMetadata query, ImmutableArray attributes) { - Dictionary variables = new(_stringComparer); + List variables = []; + HashSet existingVariables = new(_stringComparer); foreach (var attribute in attributes) { @@ -123,55 +98,54 @@ private void UpdateQueryFromAttributes(QueryMetadata query, ImmutableArray(nameof(VariableAttribute.DefaultValue), out var value) ? value : null; - variables[variableName] = new VariableMetadata(variableName, graphType, defaultValue, attribute.ApplicationSyntaxReference); + variables.Add(new VariableMetadata(variableName, graphType, defaultValue, attribute.ApplicationSyntaxReference)); + existingVariables.Add(variableName); } } } - query.Variables = [.. variables.Values]; + query.Variables = [.. variables]; } - private (TypeMetadata, List) ParseTypeMetadata(INamedTypeSymbol type) - { - if (_typeCache.TryGetValue(type, out var cachedType)) - { - return cachedType; - } - + private (TypeMetadata, List) ParseTypeMetadata(INamedTypeSymbol type, CancellationToken cancellationToken) + { INamedTypeSymbol? current = type; - - Dictionary members = new(_stringComparer); - + Dictionary members = new(_stringComparer); + HashSet skippedMembers = new(_stringComparer); List arguments = []; - while (current is not null && current.SpecialType is not SpecialType.System_Object and not SpecialType.System_ValueType) + while (current != null && current.SpecialType is not SpecialType.System_Object and not SpecialType.System_ValueType) { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var member in current.GetMembers()) { - if ((member.Kind != SymbolKind.Property && member.Kind != SymbolKind.Field) || member.IsImplicitlyDeclared) + var memberName = member.Name; + + if ((member.Kind != SymbolKind.Property && member.Kind != SymbolKind.Field) || member.IsImplicitlyDeclared || skippedMembers.Contains(memberName)) { continue; } - var memberName = member.Name; var attributes = member.GetAttributes(); - if (members.TryGetValue(memberName, out var metadata) is false) + if (!members.TryGetValue(memberName, out var metadata)) { - metadata = ParseMemberMetadata(member, out var memberType); - members[memberName] = metadata; - - if (metadata.IsSerializable is false) + if (!TryGetSerializableMemberType(member, out var memberType)) { + skippedMembers.Add(memberName); continue; } + metadata = new MemberMetadata(member.Name, member.Kind); + members[memberName] = metadata; + if (symbols.TryGetChildrenType(memberType!, out var childrenType)) { - var (childMetadata, childArguments) = ParseTypeMetadata(childrenType!); + var (childMetadata, childArguments) = ParseTypeMetadata(childrenType, cancellationToken); metadata.ChildrenType = childMetadata; arguments.AddRange(childArguments); } @@ -184,42 +158,29 @@ private void UpdateQueryFromAttributes(QueryMetadata query, ImmutableArray m.IsSerializable && m.Ignore is not true).ToArray(); - var typeMetadata = new TypeMetadata(type.ToDisplayString(), [.. typeMembers]); - - var result = (typeMetadata, arguments); - _typeCache[type] = result; - return result; - } - - private bool TryGetMemberType(ISymbol member, [NotNullWhen(true)] out INamedTypeSymbol? memberType) - { - memberType = member switch - { - IPropertySymbol property when symbols.IsPropertySerializable(property) => property.Type, - IFieldSymbol field when symbols.IsFieldSerializable(field) => field.Type, - _ => null - } as INamedTypeSymbol; + var typeMetadata = new TypeMetadata(type.ToDisplayString(), [.. members.Values]); - return memberType is not null; + return (typeMetadata, arguments); } - - private MemberMetadata ParseMemberMetadata(ISymbol member, out INamedTypeSymbol? memberType) + private bool TryGetSerializableMemberType(ISymbol member, [NotNullWhen(true)] out INamedTypeSymbol? memberType) { memberType = member switch { @@ -228,23 +189,26 @@ IFieldSymbol field when symbols.IsFieldSerializable(field) => field.Type, _ => null } as INamedTypeSymbol; - return new MemberMetadata(member.Name, member.Kind) + if (memberType == null || !symbols.IsTypeSerializable(memberType)) { - IsSerializable = memberType != null && symbols.IsTypeSerializable(memberType) - }; - } + memberType = null; + return false; + } + return true; + } + private void UpdateMemberFromBaseAttributes(MemberMetadata metadata, ImmutableArray attributes) { Dictionary arguments = new(_stringComparer); HashSet inlineFragments = new(_stringComparer); - + foreach (var attribute in attributes) { if (attribute.IsAttributeType(symbols.ArgumentAttribute)) { if (attribute.TryGetConstructorArguments(out string? name, out string? variableName) - && arguments.ContainsKey(name) is false) + && !arguments.ContainsKey(name)) { var argMetadata = new ArgumentMetadata(name, variableName, attribute.ApplicationSyntaxReference); arguments[name] = argMetadata; @@ -271,17 +235,17 @@ private void UpdateMemberFromInheritedAttributes(MemberMetadata metadata, Immuta { foreach (var attribute in attributes) { - if (attribute.IsAttributeType(symbols.JsonPropertyAttribute) && metadata.OverrideName is null) + if (attribute.IsAttributeType(symbols.JsonPropertyAttribute)) { - metadata.OverrideName = attribute.TryGetConstructorArgument(out string? overrideName) ? overrideName : null; + metadata.OverrideName ??= attribute.TryGetConstructorArgument(out string? overrideName) ? overrideName : null; } - else if (attribute.IsAttributeType(symbols.JsonIgnoreAttribute) && metadata.Ignore is null) + else if (attribute.IsAttributeType(symbols.JsonIgnoreAttribute)) { var ignoreCondition = attribute.TryGetNamedArgument(nameof(JsonIgnoreAttribute.Condition), out var condition) ? (JsonIgnoreCondition)condition : JsonIgnoreCondition.Always; - metadata.Ignore = ignoreCondition is not JsonIgnoreCondition.Never; + metadata.Ignore ??= ignoreCondition != JsonIgnoreCondition.Never; } } } diff --git a/src/QueryByShape.Analyzer/QueryGenerator.cs b/src/QueryByShape.Analyzer/QueryGenerator.cs index a8e11b4..ba185d4 100644 --- a/src/QueryByShape.Analyzer/QueryGenerator.cs +++ b/src/QueryByShape.Analyzer/QueryGenerator.cs @@ -24,13 +24,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeNames.QUERY, (node, cancellationToken) => node is ClassDeclarationSyntax or RecordDeclarationSyntax, (context, cancellationToken) => (Context: (TypeDeclarationSyntax)context.TargetNode, context.SemanticModel)) - .Collect() .Combine(symbols) - .SelectMany(static (tuple, cancellationToken) => QueryParser.Process(tuple.Left, tuple.Right, cancellationToken)); - + .Select(static (tuple, cancellationToken) => + { + var queryParser = new QueryParser(tuple.Right); + return queryParser.Parse(tuple.Left.Context, tuple.Left.SemanticModel, cancellationToken); + }); context.RegisterSourceOutput( - queryContext.SelectMany((x, _) => x.Diagnostics), + queryContext.Where(q => q.Diagnostics is not null).SelectMany((x, _) => x.Diagnostics.Value), static (context, metadata) => context.ReportDiagnostic(metadata.ToDiagnostic()) ); diff --git a/src/QueryByShape.Analyzer/QueryMetadata.cs b/src/QueryByShape.Analyzer/QueryMetadata.cs index 0151560..6e46fd5 100644 --- a/src/QueryByShape.Analyzer/QueryMetadata.cs +++ b/src/QueryByShape.Analyzer/QueryMetadata.cs @@ -26,7 +26,7 @@ internal record QueryMetadata(string TypeName, string NamespaceName, TypeMetadat public QueryOptions Options { get; set; } = new QueryOptions(); } - internal record TypeMetadata(string Name, EquatableArray? Members); + internal record TypeMetadata(string Name, EquatableArray Members); internal record MemberMetadata(string Name, SymbolKind Kind) { @@ -36,8 +36,6 @@ internal record MemberMetadata(string Name, SymbolKind Kind) public bool? Ignore { get; set; } - public bool IsSerializable { get; set; } - public EquatableArray? Arguments { get; set; } public EquatableArray? On { get; set; } diff --git a/src/QueryByShape.Analyzer/ReferenceSet.cs b/src/QueryByShape.Analyzer/ReferenceSet.cs index eeac431..25b48f4 100644 --- a/src/QueryByShape.Analyzer/ReferenceSet.cs +++ b/src/QueryByShape.Analyzer/ReferenceSet.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace QueryByShape.Analyzer { @@ -9,17 +10,17 @@ namespace QueryByShape.Analyzer /// internal sealed class ReferenceSet { - private readonly TValue[] _items; + private readonly IList _items; private readonly Dictionary _unreferenced; private readonly HashSet _initialKeys; - public ReferenceSet(TValue[] items, Func keySelector, IEqualityComparer comparer) + public ReferenceSet(IList? items, Func keySelector, IEqualityComparer comparer) { _items = items ?? Array.Empty(); - _unreferenced = new Dictionary(_items.Length, comparer); + _unreferenced = new Dictionary(_items.Count, comparer); _initialKeys = new HashSet(comparer); - for (var i = 0; i < _items.Length; i++) + for (var i = 0; i < _items.Count; i++) { var key = keySelector(_items[i]); diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.ExcludesFieldsWhenNotConfigured.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.ExcludesFieldsWhenNotConfigured.verified.txt index 546d854..83981d0 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.ExcludesFieldsWhenNotConfigured.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.ExcludesFieldsWhenNotConfigured.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesAlias.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesAlias.verified.txt index ab5917c..cf9f0ee 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesAlias.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesAlias.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariables.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariables.verified.txt index fa899ac..b735e89 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariables.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariables.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariablesWithDefaults.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariablesWithDefaults.verified.txt index 812cb61..bd5a779 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariablesWithDefaults.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesArgumentsAndVariablesWithDefaults.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesBasicQuery.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesBasicQuery.verified.txt index e4e6225..7ff9281 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesBasicQuery.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesBasicQuery.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesComplexIEnumarableQuery.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesComplexIEnumarableQuery.verified.txt index e4e6225..7ff9281 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesComplexIEnumarableQuery.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesComplexIEnumarableQuery.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFieldsWhenConfigured.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFieldsWhenConfigured.verified.txt index ab6ebed..c80e77d 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFieldsWhenConfigured.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFieldsWhenConfigured.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFragment.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFragment.verified.txt index dd7c7b4..3e64212 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFragment.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesFragment.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesKitchenSinkQuery.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesKitchenSinkQuery.verified.txt index 48c9f0b..4af58c0 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesKitchenSinkQuery.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesKitchenSinkQuery.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithDifferentOptions.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithDifferentOptions.verified.txt index 2ecf52d..74c0ff7 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithDifferentOptions.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithDifferentOptions.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings @@ -12,6 +13,7 @@ namespace Tests public static string ToGraphQLQuery() => @"query firstQuery($id:UUID!) { people(id:$id) { customerId firstName lastName middleName addressLine1 city state zipCode } }"; } } +// #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithSharedVariables.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithSharedVariables.verified.txt index b279aa1..ff2f011 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithSharedVariables.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesMultipleQueriesWithSharedVariables.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings @@ -12,6 +13,7 @@ namespace Tests public static string ToGraphQLQuery() => @"query firstQuery($id:UUID!,$orderId:UUID!) { people(id:$id) { customerId orders(orderId:$orderId) { orderId orderDate } firstName lastName middleName } }"; } } +// #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesRecordQuery.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesRecordQuery.verified.txt index 74398a6..0c6f13f 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesRecordQuery.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesRecordQuery.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonIgnore.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonIgnore.verified.txt index 12b1448..26329aa 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonIgnore.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonIgnore.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonPropertyName.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonPropertyName.verified.txt index 58b2566..0f036c6 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonPropertyName.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithJsonPropertyName.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithStructQuery.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithStructQuery.verified.txt index 1d04066..43f21b8 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithStructQuery.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.GeneratesWithStructQuery.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.SupportsQueryName.verified.txt b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.SupportsQueryName.verified.txt index ebbe24f..b9b7927 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.SupportsQueryName.verified.txt +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/Snapshots/SnapshotTests.SupportsQueryName.verified.txt @@ -1,4 +1,5 @@ - +// + #nullable enable annotations #nullable disable warnings diff --git a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/TestHelper.cs b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/TestHelper.cs index 6a858e9..58ec8f7 100644 --- a/tests/QueryByShape.Analyzer.Tests/SourceGenerator/TestHelper.cs +++ b/tests/QueryByShape.Analyzer.Tests/SourceGenerator/TestHelper.cs @@ -56,7 +56,6 @@ public static Task VerifySnapshot(string source) { return Verify(GetGeneratorResult(source)) - .ScrubLinesContaining("") .UseDirectory("Snapshots"); } } \ No newline at end of file