Skip to content

Swift: enum cases (case timeout, case server(code: Int, message: String)) never captured as symbols, and indirect enum is silently dropped because indirect is missing from the enum-row modifier slot #319

Description

@Widthdom

Summary

Two independent Swift extractor gaps in src/CodeIndex/Indexer/SymbolExtractor.cs:247-258:

  1. Enum cases are never captured. There is no row matching the case keyword inside a Swift enum body. Every case name, case name(AssociatedValues), and case name = "raw" declaration is silently dropped. Swift's enums are the idiomatic sum type for modeling domain values (Result<T,E>, NetworkError, UIKit/SwiftUI routing enums with associated values) — missing the entire case surface removes the one thing that makes Swift enums more useful than structs.

  2. indirect enum is silently dropped entirely. The Swift enum row at line 251 has no modifier slot in front of enum:

    new("enum", new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*enum\s+(?<name>\w+)", ...), BodyStyle.Brace, "visibility"),

    So indirect enum Tree<T> falls through the entire Swift row list — no symbol, no warning. Recursive algebraic data types (indirect enums with case node(Tree<T>, Tree<T>)) are the canonical Swift idiom for trees, linked lists, expression ASTs, parser combinators — all invisible.

Both are in the same enum row and fix together cleanly. Filing as a single ticket because the test fixture and scope overlap.

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/swift-enum && cat > /tmp/dogfood/swift-enum/E.swift <<'EOF'
enum NetworkError: Error {
    case timeout
    case server(code: Int, message: String)
    case client(Int)
    case unknown
}

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case triangle(a: Double, b: Double, c: Double)
}

indirect enum Tree<T> {
    case leaf(T)
    case node(Tree<T>, Tree<T>)
}

enum Status: String, Codable {
    case active
    case inactive = "off"
    case pending
}
EOF
"$CDIDX" index /tmp/dogfood/swift-enum --rebuild
"$CDIDX" symbols --db /tmp/dogfood/swift-enum/.cdidx/codeindex.db

Observed:

enum       NetworkError                             E.swift:1-6
enum       Shape                                    E.swift:8-12
enum       Status                                   E.swift:19-23
(3 symbols in 1 files)

Missing:

  • Tree — dropped entirely (bug 2: indirect modifier blocks the enum row).
  • 14 case entries across all four enums — dropped entirely (bug 1: no case row).

Expected: 4 enum type rows + 14 case rows = 18 total symbols. Today: 3.

Suspected root cause

src/CodeIndex/Indexer/SymbolExtractor.cs:247-258:

["swift"] =
[
    new("function", new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*(?:(?:static|class|nonisolated|mutating|nonmutating)\s+)*(?:override\s+)?func\s+(?<name>\w+)", ...), BodyStyle.Brace, "visibility"),
    new("struct",    new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*(?:(?:final)\s+)*struct\s+(?<name>\w+)", ...), BodyStyle.Brace, "visibility"),
    new("enum",      new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*enum\s+(?<name>\w+)", ...), BodyStyle.Brace, "visibility"),     // ← no modifier slot
    new("interface", new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*protocol\s+(?<name>\w+)", ...), BodyStyle.Brace, "visibility"),
    new("class",    new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*(?:(?:final|distributed)\s+)*(?:class|actor)\s+(?<name>\w+)", ...), BodyStyle.Brace, "visibility"),
    new("class",    new Regex(@"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*typealias\s+(?<name>\w+)", ...), BodyStyle.None, "visibility"),   // ← kind=class covered by #206
    new("import",   new Regex(@"^\s*import\s+(?<name>.+)", ...), BodyStyle.None),
],
  1. Enum row has no case sibling. The closest to enum-value capture is the class/actor row, which doesn't help. The typealias row has BodyStyle.None and is kind-misclassified (separately tracked by Type-alias declarations misclassified as class across Rust / TypeScript / Swift / Go / F# (and silently dropped in Scala) #206 for Swift/Rust/TS/Go/F#).

  2. Enum row's modifier slot missing indirect. The class/actor row has (?:(?:final|distributed)\s+)* and the struct row has (?:(?:final)\s+)*, but the enum row has no modifier slot at all. Swift's indirect keyword applies either at the enum level (entire enum is boxed) or at the individual case level (indirect case node(...)).

Suggested direction

Two additive fixes in the same row block:

Fix 1 — add indirect to the enum modifier slot:

// Add modifier slot to enum row for indirect / Swift enum 行に indirect 修飾子スロットを追加。
new("enum",
    new Regex(
        @"^\s*(?<visibility>public|private|internal|open|fileprivate)?\s*"
      + @"(?:indirect\s+)?"                                                      // ← NEW: indirect modifier
      + @"enum\s+(?<name>\w+)",
        RegexOptions.Compiled),
    BodyStyle.Brace,
    "visibility"),

indirect can only combine with enum in Swift (not with struct, class, actor), so this slot is enum-specific and doesn't risk widening the other rows.

Fix 2 — add an enum-case row after the enum row:

// Swift enum case — bare, with associated values, or with raw value.
// Swift 列挙ケース — bare / 関連値 / 生値いずれにも対応。
new("property",
    new Regex(
        @"^\s*(?:indirect\s+)?"                              // indirect case is legal at the case level too
      + @"case\s+(?<name>\w+)"                               // case name
      + @"(?:\s*\(.*\))?"                                    // optional associated-values tuple
      + @"(?:\s*=\s*(?<returnType>.+?))?"                    // optional raw-value assignment
      + @"\s*$",
        RegexOptions.Compiled),
    BodyStyle.None,
    ReturnTypeGroup: "returnType"),

Notes:

  • Kind choice. property matches the treatment Kotlin uses for enum-like value entries. Alternate option: introduce a new case or enum_member kind, but that requires taxonomy discussion — property lands the visibility/searchability gains immediately without schema debate.
  • Associated value capture. The (?:\s*\(.*\))? group accepts case server(code: Int, message: String). The captured string is not parsed further here; downstream signature extraction could be improved in a follow-up, but the symbol's existence and name are the high-value gain.
  • Raw value capture. The = ... suffix only applies to backed enums (enum Status: String). The captured value feeds return_type metadata, matching the treatment suggested for PHP enums in the sibling ticket.
  • Comma-list cases. Swift also allows case foo, bar, baz on a single line (less idiomatic but legal for bare cases). The proposed row captures only the first name via \w+. Covering comma-lists requires a split similar to the CSS comma-selector handling from CSS: comma-joined selector lists drop every selector after the first on the same line; ID selectors misclassified as kind=function; @counter-style / @page / @namespace at-rules not captured #235 — out of scope for this ticket, can be added later if it shows up in real codebases.
  • Switch-case false positives. Swift switch arms have the form case .timeout: (with dot-shorthand) or case let .server(code, _): (with binding). Both start with case but are followed by . or let, not a bare identifier. The proposed pattern requires case\s+\w+ without a leading ./let, and the trailing \s*$ anchors the match to the full declaration line. A switch arm ending in : would fail the \s*$ anchor (Swift switch arms require : before the arm body on the same or next line). Verify with a fixture.

Why it matters

  • Every UIKit/AppKit/SwiftUI codebase models routing, navigation, state, error handling via enum with associated values. cdidx definition timeout --lang swift returns zero today.
  • indirect enum is the canonical Swift recursive-type idiom. Parser combinators, tree/AST libraries (SwiftSyntax's own data model), linked lists — all invisible. Not captured at all, not even as a class.
  • unused over-reports. public enum Foo { case bar } with bar never extracted means the value is flagged as unused (because there are no case definitions and Foo is only mentioned as the type, not dereferenced). Even without graph support (Swift's graph column is no per cdidx languages), symbol-level searches fail.
  • outline / inspect show enum shells with no interior, same as the PHP sibling.
  • Silent. The enum type shell captures successfully, giving the user a false sense that the file is indexed.

Cross-language note

  • PHP 8.1+ enum cases — same structural gap, filed as a sibling ticket. The fix regex differs (PHP uses ; terminator; Swift uses newline with optional = value).
  • Rust enum variantsenum Foo { Bar, Baz(i32) } — worth a spot-check, different syntax shape (comma-separated, multi-line bodies).
  • Java enumsenum E { A, B; } — already a long-standing pattern; worth verifying with a fixture.
  • Kotlin enum class — enum entries like FOO, / BAR(42) — worth a spot-check.
  • C# — comma-separated enum members — Covered correctly today.
  • indirect modifier — Swift-only, so no cross-language action.

Scope

  • src/CodeIndex/Indexer/SymbolExtractor.cs:247-258 — two additive changes in the Swift row block: (a) widen enum row's modifier slot to include indirect, (b) add a new enum-case row with optional associated-values tuple and optional raw-value suffix.
  • tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures: (a) bare case case timeout captured as property with name=timeout, (b) associated-values case case server(code: Int, message: String) captured with name server, (c) raw-value case case inactive = "off" captured with backing value in return_type, (d) indirect enum Tree<T> captured as enum, (e) indirect case node(Tree<T>, Tree<T>) captured, (f) regression: Swift switch case .foo: / case let .bar(x, y): arms are NOT captured, (g) regression: enum type enum NetworkError: Error still matches and is not disturbed by the new modifier slot.
  • DEVELOPER_GUIDE.md — Swift language pattern reference row should list case + indirect support.

Related

Environment

  • cdidx v1.10.0 at /root/.local/bin/cdidx (trimmed release build installed via install.sh).
  • Fixture /tmp/dogfood/swift-enum/E.swift as above.
  • cdidx languages shows swift with yes / no in Symbols/Graph (graph not supported, so reference-side enum-case gaps are out of scope for this ticket — symbol-side only).
  • Platform: linux-x64 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