You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Java enum member pattern over-matches: any UPPERCASE(...); call statement becomes a phantom function definition — fires outside enum bodies and inside text blocks #292
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:
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).
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",newRegex(@"^\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:
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.
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",newRegex(@"^\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
C# enum members — defined with = value form, no parens, no matching ambiguity. Not affected by this regex shape.
Kotlin enum class — parenthesized constructor calls like RED(0xFF0000), but Kotlin has its own symbol-extractor block (SymbolExtractor.cs:186+). Worth checking whether SymbolExtractor.cs Kotlin patterns have the same over-match.
Swift enum cases — case success, failure, pending form. Different regex; not affected here.
Protobuf enum values — same UPPER_CASE = 0; form but different language, not affected.
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.
Summary
The Java enum-member pattern in
SymbolExtractor.cs:183is 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:COMPUTE(5);,ASSERT_EQ(a, b);,MAX_VALUE(1, 2);). These get recorded asfunctiondefinitions at the call site, pointing thedefinition/symbolsread paths at the wrong line (or at a line with no definition at all)."""..."""text blocks (e.g.BadMethod(arg);,AnotherPhantom('x');).SymbolExtractorhas 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 fakeclass Approws on cdidx itself, topmapentrypoint 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.Nonesymbols (zero-line, no body range), but they still show up indefinition,outline, andsymbolsas authoritative function definitions.Downstream impact:
definition UPPER_NAMEpoints at the call site instead of "not found."outline File.javalists phantom functions in the middle of method bodies and string fixtures, polluting the per-file map.references UPPER_NAMEfilters the phantom out of the reference graph:ReferenceExtractor.ExtractatReferenceExtractor.cs:160-161skips call matches whose name appears indefinitionNamesByLinefor the same line. So the call looks like a definition and the reference edge disappears, breakingcallers/impactfor any uppercase-named method.ASSERT_EQdefined?" get a wrong-line pointer; asking "who callsCOMPUTE?" gets zero hits whenCOMPUTE(5);is the only call in the file.Repro
Observed (actual):
Expected: only
class Main,function run,function doWorkin 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: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 afunction— regardless of whether we're inside an enum, a method, a text block, or a multi-line string.Two independent issues combine:
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.No multi-line-string awareness. Java text blocks
"""..."""(Java 15+) span multiple lines.SymbolExtractorfeeds 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 fakeclass Approws on cdidx itself, topmapentrypoint is a Lua fixture string #177 / ReferenceExtractor leaks calls out of C# multi-line strings —@"..."verbatim bodies and"""..."""raw-string bodies produce phantomcall/instantiatereference rows #274 on the C# side), so a line likeBadMethod(arg);inside a text block is indistinguishable from a method-body statement.Quick ranking of which real-world lines misfire:
ASSERT_EQ(a, b);COMPUTE(5);COMPUTEdefinitionBadMethod(arg);(inside"""...""")Helper.doSomething();Helperfollowed by., so[A-Z]\w*stops atHelperand\s*(?:\(.*\))?fails — real calls with qualified receivers are safedoWork();So the pattern specifically over-catches uppercase-named top-level calls (constants-as-methods, assertion helpers, test DSL builders, any
SCREAMING_SNAKEentry point). In idiomatic Java the convention is lowercase method names, so much of the wild will be unaffected, but:assertThat-style helpers with uppercase-named variants (ASSERT_EQ,EXPECT_THAT,CHECK_OK) is a hot spot."""..."""text block — SQL fixtures, JSON fixtures, HTML/XML fixtures — will see every uppercase-startingWord(...);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;/MONDAYat end-of-block before}MONDAY(1)/MONDAY(1, "Mon")with constructor argsTightening 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:[A-Z][A-Z0-9_]*restricts toSCREAMING_SNAKE— matches real enum constants (MONDAY,HTTP_OK,MAX_RETRIES) while excludingBadMethod/AnotherPhantom/AssertEq(mixed case). This alone would have killed all four phantoms in the Case 2 repro and reduced Case 1 to justMAX_VALUE+ASSERT_EQ(still wrong, butCOMPUTE/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 withSymbolExtractorand 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
inEnumBodyflag 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
enummembers — defined with= valueform, no parens, no matching ambiguity. Not affected by this regex shape.enum class— parenthesized constructor calls likeRED(0xFF0000), but Kotlin has its own symbol-extractor block (SymbolExtractor.cs:186+). Worth checking whetherSymbolExtractor.csKotlin patterns have the same over-match.enumcases —case success, failure, pendingform. Different regex; not affected here.enumvalues — sameUPPER_CASE = 0;form but different language, not affected."""..."""raw strings (and other multi-line strings) becomes phantom symbols — 52 fakeclass Approws on cdidx itself, topmapentrypoint is a Lua fixture string #177 / ReferenceExtractor leaks calls out of C# multi-line strings —@"..."verbatim bodies and"""..."""raw-string bodies produce phantomcall/instantiatereference rows #274 (which focus on C#). Fixing B here would be the natural place to introduce a shared multi-line-string masking helper, then reuse it for the Python / Rust / Kotlin raw-string cousins.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 phantomcall/instantiatereference rows #274 lands, Java also gets text-block masking on the reference side. Today the phantom symbols suppress reference edges viaReferenceExtractor.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 withMONDAY,/MONDAY(1, "Mon"),members still captured asfunction(or consider promoting them to a dedicatedpropertykind).DEVELOPER_GUIDE.mdlanguage-pattern reference table — Java row should note the enum-member tightening and (if B is adopted) text-block masking.Related
"""..."""raw strings (and other multi-line strings) becomes phantom symbols — 52 fakeclass Approws on cdidx itself, topmapentrypoint is a Lua fixture string #177 — Parent family. C# multi-line strings produce phantomclass/functionsymbols. Java text blocks have the identical root cause on the symbol side; this ticket adds the Java-specific misfire and combines it with the enum-member regex over-match.@"..."verbatim bodies and"""..."""raw-string bodies produce phantomcall/instantiatereference rows #274 — C# reference-side leak from multi-line strings. The shared text-block masking helper suggested above is the same one proposed there."""..."""raw strings and@"..."verbatim strings produce phantom reference rows —StringLiteralRegexis single-line only, so every multi-line string body bleeds its contents into the reference graph #288 — C# reference-side phantom calls from"""..."""raw strings. Same root cause family as this issue's Case 2.else if,case const Class()etc. asfunction if/function Class— negative lookahead missingelse/case/ more #163 — Dart else-if /case const Class()false positives. Different language, same family of "per-line regex matches non-declaration shape."enummis-kinded asclass,package/oneof/extendnever captured (5,815 enum + 6,932 package + 2,572 oneof misses on googleapis) #172 — Protobuf enum / namespace / oneof coverage. Different language, adjacent concern (enum handling).IgnoredCallNamesis language-global. Unrelated, but lives in the same ReferenceExtractor and gets confused by the same uppercase-call shape.Environment
install.shinto/root/.local/bin/cdidx).CLOUD_BOOTSTRAP_PROMPT.md.