Skip to content

Java enum member pattern over-matches: any UPPERCASE(...); call statement becomes a phantom function definition — fires outside enum bodies and inside text blocks #292

Description

@Widthdom

Summary

The Java enum-member pattern in SymbolExtractor.cs:183 is not anchored to an enum body. It matches any line that starts with 2+ spaces, an uppercase identifier, an optional (...), and terminates with ,, {, or ;. In practice the pattern fires on two common idioms that are not enum members:

  1. Call statements to uppercase-named methods in ordinary method bodies (e.g. COMPUTE(5);, ASSERT_EQ(a, b);, MAX_VALUE(1, 2);). These get recorded as function definitions at the call site, pointing the definition / symbols read paths at the wrong line (or at a line with no definition at all).
  2. Call-shaped lines inside Java 15+ """...""" text blocks (e.g. BadMethod(arg);, AnotherPhantom('x');). SymbolExtractor has no multi-line-string state — same root cause as Symbol extractor doesn't track string-literal boundaries: code inside C# """...""" raw strings (and other multi-line strings) becomes phantom symbols — 52 fake class App rows on cdidx itself, top map entrypoint is a Lua fixture string #177 on the C# side — so text-block contents are delivered to the per-line regex list as if they were code. The enum-member regex is by far the greediest Java pattern, so almost any uppercase-starting call-shaped line inside a text block becomes a phantom symbol.

Both cases produce BodyStyle.None symbols (zero-line, no body range), but they still show up in definition, outline, and symbols as authoritative function definitions.

Downstream impact:

  • definition UPPER_NAME points at the call site instead of "not found."
  • outline File.java lists phantom functions in the middle of method bodies and string fixtures, polluting the per-file map.
  • references UPPER_NAME filters the phantom out of the reference graph: ReferenceExtractor.Extract at ReferenceExtractor.cs:160-161 skips call matches whose name appears in definitionNamesByLine for the same line. So the call looks like a definition and the reference edge disappears, breaking callers / impact for any uppercase-named method.
  • AI consumers asking "where is ASSERT_EQ defined?" get a wrong-line pointer; asking "who calls COMPUTE?" gets zero hits when COMPUTE(5); is the only call in the file.

Repro

CDIDX=/root/.local/bin/cdidx

# Case 1: uppercase-named method call statements in an ordinary method body
# ケース1: 通常のメソッド本体内の大文字名メソッド呼び出し文
mkdir -p /tmp/dogfood/java-upper && cat > /tmp/dogfood/java-upper/Caller.java <<'EOF'
public class Caller {
    public static void main(String[] args) {
        COMPUTE(5);
        MAX_VALUE(1, 2);
        ASSERT_EQ(a, b);
    }
    public static int COMPUTE(int x) { return x; }
}
EOF
"$CDIDX" index /tmp/dogfood/java-upper --rebuild
DB1=/tmp/dogfood/java-upper/.cdidx/codeindex.db

echo '--- symbols ---'
"$CDIDX" symbols --db "$DB1"
echo '--- definition MAX_VALUE ---'
"$CDIDX" definition MAX_VALUE --db "$DB1"
echo '--- definition ASSERT_EQ ---'
"$CDIDX" definition ASSERT_EQ --db "$DB1"

# Case 2: text-block contents captured as phantom functions
# ケース2: text block の中身が phantom function として捕捉される
mkdir -p /tmp/dogfood/java-textblock && cat > /tmp/dogfood/java-textblock/Main.java <<'EOF'
public class Main {
    public static void run() {
        String sql = """
            SELECT * FROM users
            WHERE id IN (SELECT id FROM admins)
            AND name = 'PhantomCall(42)'
            """;
        String script = """
            BadMethod(arg);
            AnotherPhantom('x');
            """;
        doWork();
    }
    public static void doWork() {}
}
EOF
"$CDIDX" index /tmp/dogfood/java-textblock --rebuild
DB2=/tmp/dogfood/java-textblock/.cdidx/codeindex.db

echo '--- symbols ---'
"$CDIDX" symbols --db "$DB2"
echo '--- definition BadMethod ---'
"$CDIDX" definition BadMethod --db "$DB2"
echo '--- definition AnotherPhantom ---'
"$CDIDX" definition AnotherPhantom --db "$DB2"

Observed (actual):

--- Case 1 symbols ---
function   COMPUTE       Caller.java:7       ← real definition
class      Caller        Caller.java:1-8
function   main          Caller.java:2-6
function   ASSERT_EQ     Caller.java:5       ← phantom (from `ASSERT_EQ(a, b);`)
function   COMPUTE       Caller.java:3       ← phantom (from `COMPUTE(5);` — duplicates the real one!)
function   MAX_VALUE     Caller.java:4       ← phantom (from `MAX_VALUE(1, 2);`)

--- Case 1 definition MAX_VALUE ---
function   MAX_VALUE     Caller.java:4-4 in Caller
     4:         MAX_VALUE(1, 2);       ← call site presented as the definition

--- Case 2 symbols ---
class      Main              Main.java:1-16
function   run               Main.java:2-14
function   doWork            Main.java:15
function   BadMethod         Main.java:9     ← phantom (inside """...""")
function   AnotherPhantom    Main.java:10    ← phantom (inside """...""")

--- Case 2 definition BadMethod ---
function   BadMethod     Main.java:9-9 in Main
     9:             BadMethod(arg);        ← text-block content presented as a Java function

Expected: only class Main, function run, function doWork in Case 2; class Caller, function main, function COMPUTE (line 7 only) in Case 1. No phantom functions for call statements, and no symbols at all inside text-block bodies.

Suspected root cause (from reading the source)

src/CodeIndex/Indexer/SymbolExtractor.cs:181-183:

// Enum member (uppercase constant) — 2+ whitespace for any indent style (2-space, 4-space, tab)
// enum メンバー(大文字定数)— 任意のインデントスタイル対応(2スペース、4スペース、タブ)
new("function", new Regex(@"^\s{2,}(?<name>[A-Z]\w*)\s*(?:\([^)]*\))?\s*(?:,|\{|;)\s*$", RegexOptions.Compiled), BodyStyle.None),

This pattern has no context about whether the current line is inside an enum X { ... } body. The extractor loop walks lines top-to-bottom and applies every Java pattern to every line, so any line that happens to match the shape ^\s{2,}[A-Z]\w*\s*(?:\(.*\))?\s*[,\{;]\s*$ is recorded as a function — regardless of whether we're inside an enum, a method, a text block, or a multi-line string.

Two independent issues combine:

  1. Missing context anchor. The Java enum-member form is only valid inside enum Foo { A, B, C; ... }. Outside of that, ASSERT_EQ(a, b); is a call expression, not a declaration. The regex cannot tell the difference because it has no state about the enclosing construct.

  2. No multi-line-string awareness. Java text blocks """...""" (Java 15+) span multiple lines. SymbolExtractor feeds each line independently to the pattern list (same line-by-line loop referenced by Symbol extractor doesn't track string-literal boundaries: code inside C# """...""" raw strings (and other multi-line strings) becomes phantom symbols — 52 fake class App rows on cdidx itself, top map entrypoint is a Lua fixture string #177 / ReferenceExtractor leaks calls out of C# multi-line strings — @"..." verbatim bodies and """...""" raw-string bodies produce phantom call / instantiate reference rows #274 on the C# side), so a line like BadMethod(arg); inside a text block is indistinguishable from a method-body statement.

Quick ranking of which real-world lines misfire:

Line Matches? Why it's wrong
ASSERT_EQ(a, b); Call expression inside a method body, not an enum member
COMPUTE(5); Call expression; also duplicates the real COMPUTE definition
BadMethod(arg); (inside """...""") Text-block content, not code at all
Helper.doSomething(); Helper followed by ., so [A-Z]\w* stops at Helper and \s*(?:\(.*\))? fails — real calls with qualified receivers are safe
doWork(); Starts lowercase; real method names per Java convention dodge the trap

So the pattern specifically over-catches uppercase-named top-level calls (constants-as-methods, assertion helpers, test DSL builders, any SCREAMING_SNAKE entry point). In idiomatic Java the convention is lowercase method names, so much of the wild will be unaffected, but:

  • Test code using assertThat-style helpers with uppercase-named variants (ASSERT_EQ, EXPECT_THAT, CHECK_OK) is a hot spot.
  • Kotlin-interop code calling Java static constants via methods.
  • Generated code, protocol bindings, etc. that use uppercase method names.
  • Any Java file with a """...""" text block — SQL fixtures, JSON fixtures, HTML/XML fixtures — will see every uppercase-starting Word(...); line inside the block promoted to a symbol.

Suggested direction

Two options, independent but ideally both:

A. Restrict the enum-member pattern to uppercase identifiers that look like enum constants, not calls. Drop the optional (...) group, or require the whole line to not contain a (. Real enum members usually have one of these shapes:

  • MONDAY, / MONDAY; / MONDAY at end-of-block before }
  • MONDAY(1) / MONDAY(1, "Mon") with constructor args

Tightening the regex to require the terminator , or the closing ; or { to immediately follow the name (optionally after (...) that closes on the same line and contains no nested parens) — and excluding anything that looks like a statement — would cut the false-positive rate drastically. Concrete sketch:

// Enum members only: name optionally followed by a single-paren constructor call,
// then required comma/semicolon/brace. Statements with a trailing `;` that contain
// a call expression (like `FOO(x);` with no surrounding enum context) are still ambiguous,
// so pair this with option B.
// enum メンバー専用: 名前の後ろはオプションで単層括弧の constructor call、
// その後に必須の , / ; / { が続く形。呼び出し文との区別は option B と併用する。
new("function", new Regex(@"^\s{2,}(?<name>[A-Z][A-Z0-9_]*)\s*(?:\([^()\n]*\))?\s*(?:,|;|\{)\s*$", RegexOptions.Compiled), BodyStyle.None),

[A-Z][A-Z0-9_]* restricts to SCREAMING_SNAKE — matches real enum constants (MONDAY, HTTP_OK, MAX_RETRIES) while excluding BadMethod / AnotherPhantom / AssertEq (mixed case). This alone would have killed all four phantoms in the Case 2 repro and reduced Case 1 to just MAX_VALUE + ASSERT_EQ (still wrong, but COMPUTE / BadMethod-style get dropped).

B. Mask Java """...""" text-block bodies before the per-line symbol pass. Same structural fix proposed in #274 for ReferenceExtractor: walk the full content once with a tiny state machine, replace the interior of every """...""" block with spaces (preserving newlines and column positions), then feed the masked content into the existing per-line loop. The helper could be shared with SymbolExtractor and with any future C# / Python / Kotlin fix. Closes Case 2 completely regardless of what happens to the enum-member regex.

C. (Stretch) State-aware pattern matching. Track an inEnumBody flag through the line loop and only apply the enum-member pattern when it's set. Much larger change; probably not worth it compared to A+B. Noted for completeness.

A+B together eliminate both observed misfires without removing real enum-member support.

Cross-language note

Scope

  • src/CodeIndex/Indexer/SymbolExtractor.cs — tighten the Java enum-member regex at :183 (option A) and/or add text-block masking (option B).
  • src/CodeIndex/Indexer/ReferenceExtractor.cs — if the shared helper from ReferenceExtractor leaks calls out of C# multi-line strings — @"..." verbatim bodies and """...""" raw-string bodies produce phantom call / instantiate reference rows #274 lands, Java also gets text-block masking on the reference side. Today the phantom symbols suppress reference edges via ReferenceExtractor.cs:160-161, so option B indirectly fixes the reference graph too.
  • tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures for: (a) COMPUTE(5); in a method body is not captured, (b) ASSERT_EQ(a, b); not captured, (c) BadMethod(arg); inside """...""" not captured, (d) real enum with MONDAY, / MONDAY(1, "Mon"), members still captured as function (or consider promoting them to a dedicated property kind).
  • DEVELOPER_GUIDE.md language-pattern reference table — Java row should note the enum-member tightening and (if B is adopted) text-block masking.

Related

Environment

  • cdidx v1.10.0 (installed via published install.sh into /root/.local/bin/cdidx).
  • Platform: linux-x64 cloud container.
  • Filed from a Claude Code cloud session per CLOUD_BOOTSTRAP_PROMPT.md.

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