Skip to content

C# parser: base-list clause + range/index syntax silently zeroes out entire file (no error) #2

Description

@AsiaOstrich

Summary

Via the index_code MCP tool (equivalent to egr index's underlying parser), C# (.cs) source is now parsed on engramgraph@0.7.0 — a real improvement over what we'd observed before. However, we found a silent whole-file parse failure: certain common, valid C# constructs cause index_code to return functions: 0, classes: 0 for the entire file, with no error, warning, or partial result — indistinguishable from "this file legitimately has no functions."

This is worse than "C# unsupported," because it looks like success. A consumer indexing a codebase has no signal that a file silently produced nothing.

Minimal repro

Confirmed via bisection (isolating one variable at a time) on engramgraph@0.7.0, called through the MCP index_code tool.

Fails — returns {functions: 0, classes: 0, ...} for the whole file:

namespace Test;

public sealed class PiiHasherFullIface : ISomeFreshUnusedInterfaceName123
{
    private readonly string _version;
    private readonly int _tokenLength;
    private readonly byte[] _pepper;

    public PiiHasherFullIface(string pepper)
    {
        _version = "v1";
        _tokenLength = 8;
        _pepper = Encoding.UTF8.GetBytes(pepper);
    }

    public string HashPhone(string phone)
    {
        var normalized = (phone ?? string.Empty).Trim();
        var hash = HMACSHA256.HashData(_pepper, Encoding.UTF8.GetBytes(normalized));
        var hex = Convert.ToHexString(hash).ToLowerInvariant();
        return $"{_version}:{hex[.._tokenLength]}";
    }

    public string MaskPhone(string phone)
    {
        var p = (phone ?? string.Empty).Trim();
        if (p.Length < 8)
        {
            return p.Length <= 1 ? "***" : $"{p[0]}***{p[^1]}";
        }
        return $"{p[..4]}***{p[^3..]}";
    }
}

Passes — identical body, only the : ISomeFreshUnusedInterfaceName123 base-list clause removed:

namespace Test;

public sealed class PiiHasherFull
{
    // ...same fields/ctor/methods as above, no base-list clause...
}

→ returns {functions: 3, classes: 1, ...} correctly.

The interface referenced in the base-list clause (ISomeFreshUnusedInterfaceName123) is never declared anywhere in the call — this isn't about resolving it, just about the base-list syntax co-occurring with the method bodies below.

Isolated variables that do NOT reproduce it alone

Each of these, individually, parses fine:

  • A class implementing an undeclared interface, with a simple method body (no range/index syntax) — functions: 1/2, correct.
  • A method using C# 8 range/index syntax (hex[..4], p[^1]) in a class with no base-list clause — parses fine.
  • Tuple return types ((int Length, string Sha8) Foo(...)) — parses fine.
  • Relational patterns (x is >= 4 and <= 64) — parses fine.
  • Index expressions inside string interpolation ($"{p[0]}***{p[^1]}") — parses fine.
  • 4-method class with tuple return + no base-list — parses fine (functions: 4).

Only the combination of (a) a base-list clause (: SomeInterface) on the class and (b) range/index syntax ([.._x], [^1], [^3..]) somewhere in that class's method bodies reproduces the total-file failure.

Second, separate failure mode found in the same session

Indexing an interface declaration and a class implementing that same interface in the same index_code call (regardless of same-file or cross-file) also zeroes out the result for the entire batch, including unrelated files passed in the same call:

// file A
public interface IMultiProbe
{
    string M1(string phone);
}

// file B (or same file)
public sealed class MultiProbeIface : IMultiProbe
{
    private readonly string _a;
    public MultiProbeIface(string a) { _a = a; }
    public string M1(string phone) { return phone; }
}

→ whole batch (including any other unrelated files passed alongside) returns all-zero counts.

Re-indexing the same interface name (IMultiProbe) later in a separate call, without also declaring the interface in that call, parses fine — so this isn't a persistent poisoned-symbol issue, it's specific to interface-decl + implementation being resolved together within one call.

Why this matters in practice

Real-world C# codebases (ours included, an Onion/Clean-Architecture .NET project where nearly every concrete service class implements an interface for DI) hit failure mode #1 constantly once any modern C# 8+ range/index syntax is used anywhere in the class — which is common. Because the failure is silent (no error field, no partial extraction, no logged diagnostic), a caller doing a bulk codebase index has no way to tell "this file has 0 functions" from "the parser gave up on this file" — the returned graph looks complete but is actually missing an unknown fraction of real code.

Suggested remediation

  • Surface a parse-error/parse-warning signal per file in the index_code response (even just {path, error: "..."}, distinct from a legitimately empty file) so silent data loss is at least detectable.
  • Root-cause and fix the underlying tree-sitter grammar/query interaction between C# base-list clauses and range/index expressions.
  • Consider whether the query used to extract function/class nodes is (incorrectly) scoped such that any node-type it doesn't expect anywhere in the file aborts extraction for the whole file, rather than degrading gracefully per-declaration.

Environment

  • engramgraph@0.7.0 (npm registry latest as of 2026-07-20 is also 0.7.0, so not fixed by upgrading)
  • Reproduced via the index_code MCP tool (egr mcp server), Windows 11, Node 24
  • All repro snippets above are self-contained minimal cases, not excerpts requiring project context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions