< Summary

Information
Class: NGql.Core.Extensions.QueryDefinitionExtensions
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Extensions/QueryDefinitionExtensions.cs
Line coverage
100%
Covered lines: 39
Uncovered lines: 0
Coverable lines: 39
Total lines: 117
Line coverage: 100%
Branch coverage
100%
Covered branches: 30
Total branches: 30
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
NavigatePath(...)100%88100%
WalkPath(...)100%1212100%
FindFieldRecursively(...)100%11100%
FindFieldRecursivelyCore(...)100%88100%
NameOrAliasMatches(...)100%22100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Extensions/QueryDefinitionExtensions.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using System.Text;
 3using NGql.Core.Abstractions;
 4using NGql.Core.Features;
 5using NGql.Core.Pooling;
 6
 7namespace NGql.Core.Extensions;
 8
 9/// <summary>
 10/// Extension methods for QueryDefinition and field navigation/lookup utilities.
 11/// </summary>
 12internal static class QueryDefinitionExtensions
 13{
 14    /// <summary>
 15    /// Navigates through a dot-separated path using name or alias matching.
 16    /// Returns the final field and optionally builds the resolved path.
 17    /// Optimized with ReadOnlySpan to avoid allocations.
 18    /// </summary>
 19    /// <param name="fields">Starting field collection</param>
 20    /// <param name="path">Dot-separated path (can use ReadOnlySpan for zero-alloc)</param>
 21    /// <param name="resolvedPath">Optional: outputs the actual path with field keys (not aliases)</param>
 22    /// <param name="prependPath">Optional: path to prepend to resolvedPath</param>
 23    /// <returns>The final field definition, or null if not found</returns>
 24    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 25    internal static FieldDefinition? NavigatePath(
 26        IReadOnlyDictionary<string, FieldDefinition>? fields,
 27        ReadOnlySpan<char> path,
 28        out string? resolvedPath,
 29        string? prependPath = null)
 30    {
 39931        resolvedPath = null;
 41432        if (fields is null || path.Length == 0) return null;
 33
 34        // Build the resolved path into a pooled builder while walking — replaces the prior
 35        // List<string> + string.Join + prepend-concat trio with a single final ToString.
 38436        using var pooled = LockFreeStringBuilderPool.Get();
 38437        var pathBuilder = pooled.StringBuilder;
 54938        if (prependPath is not null) pathBuilder.Append(prependPath).Append('.');
 39
 38440        var currentField = WalkPath(fields, path, pathBuilder);
 43541        if (currentField is null) return null;
 42
 33343        resolvedPath = pathBuilder.ToString();
 33344        return currentField;
 38445    }
 46
 47    private static FieldDefinition? WalkPath(IReadOnlyDictionary<string, FieldDefinition> fields, ReadOnlySpan<char> pat
 48    {
 38449        var currentFields = fields;
 38450        FieldDefinition? currentField = null;
 38451        var firstSegment = true;
 52
 82853        while (path.Length > 0)
 54        {
 82855            var dotIndex = path.IndexOf('.');
 82856            var segment = dotIndex >= 0 ? path[..dotIndex] : path;
 57
 82858            var match = PreserveExtensions.FindFieldByNameOrAlias(currentFields, segment);
 87659            if (!match.HasValue) return null;
 60
 78061            currentField = match.Value.Value;
 120662            if (!firstSegment) resolvedPath.Append('.');
 78063            firstSegment = false;
 78064            resolvedPath.Append(match.Value.Key);
 65
 78066            if (dotIndex < 0) break;
 45067            if (currentField._children is null) return null;
 68
 44469            currentFields = currentField.Fields;
 44470            path = path[(dotIndex + 1)..];
 71        }
 72
 33373        return currentField;
 74    }
 75
 76    /// <summary>
 77    /// Recursively searches for a field by name through all descendant nodes.
 78    /// Returns all matching paths.
 79    /// </summary>
 80    internal static List<string> FindFieldRecursively(
 81        IReadOnlyDictionary<string, FieldDefinition> fields,
 82        string fieldName,
 83        string basePath)
 84    {
 2785        var results = new List<string>();
 2786        FindFieldRecursivelyCore(fields, fieldName, basePath, results);
 2787        return results;
 88    }
 89
 90    private static void FindFieldRecursivelyCore(
 91        IReadOnlyDictionary<string, FieldDefinition> fields,
 92        string fieldName,
 93        string basePath,
 94        List<string> results)
 95    {
 28296        foreach (var (key, fieldDef) in fields)
 97        {
 9698            var currentPath = string.IsNullOrEmpty(basePath) ? key : $"{basePath}.{key}";
 99
 96100            if (NameOrAliasMatches(fieldDef, fieldName))
 101            {
 21102                results.Add(currentPath);
 103            }
 104
 96105            if (fieldDef.HasFields)
 106            {
 18107                FindFieldRecursivelyCore(fieldDef._children!, fieldName, currentPath, results);
 108            }
 109        }
 45110    }
 111
 112    // A null or empty Alias needs no separate guard: string.Equals never matches the caller's
 113    // `name`, which is always a non-empty LINQ member name (see ExpressionPreservationProcessor).
 114    private static bool NameOrAliasMatches(FieldDefinition field, string name)
 96115        => string.Equals(field.Name, name, StringComparison.OrdinalIgnoreCase)
 96116        || string.Equals(field.Alias, name, StringComparison.OrdinalIgnoreCase);
 117}