package contracts import ( "regexp" "strings" ) // tsFieldRe matches a single member line inside a TS interface or // type-literal: // // name?: Type → optional // readonly name: Type → required (readonly flag ignored) // 'quoted-name': Type → required, wire name is the literal // name: string | null → optional (nullable union) // name: Array → repeated // // Capture groups: // // 1: field name (may include quotes) // 2: "?" when optional declared at the field level // 3: type expression to the end of the line, trailing comma / semicolon trimmed var tsFieldRe = regexp.MustCompile(`^\s*(?:readonly\s+)?(['"]?[\w$-]+['"]?)\s*(\??)\s*:\s*(.+?)\s*[,;]?\s*(?://\s*(.*))?$`) // extractTSShape reads a TypeScript interface or type literal and // returns its declared members. // // Supported shapes: // // interface Foo { a: string; b?: number } // type Foo = { a: string; b?: number } // interface Foo extends Bar { a: string } // class Foo { a: string; b?: number; method(): void { ... } } // // Methods on classes are filtered out by the line pattern: lines // containing `(` before the first `:` don't match tsFieldRe. func extractTSShape(src []byte, startLine, endLine int) *Shape { body := sliceBody(src, startLine, endLine) if body == "" { body = braceBody(src, startLine, 300) } openIdx := strings.Index(body, "{") if openIdx < 0 { return nil } depth := 0 closeIdx := -1 for i := openIdx; i < len(body); i++ { switch body[i] { case '{': depth++ case '}': depth-- if depth == 0 { closeIdx = i } } if closeIdx >= 0 { break } } if closeIdx < 0 { return nil } inner := body[openIdx+1 : closeIdx] kind := "interface" firstLine := body[:openIdx] if strings.Contains(firstLine, " type ") || strings.HasPrefix(strings.TrimSpace(firstLine), "type ") { kind = "type" } else if strings.Contains(firstLine, " class ") || strings.HasPrefix(strings.TrimSpace(firstLine), "class ") { kind = "class" } shape := &Shape{Kind: kind} // Track brace depth across the inner body so we don't hoist // fields of a nested anonymous object into the parent shape. // Lines are scanned at depth 0 (= immediately inside the outer // `{`); when an inline `stats: { ... }` opens, depth goes to 1 // and child lines are suppressed until the closing `}` returns // depth to 0. Without this, a parent that happens to share field // names with its inline anonymous substruct (e.g. DashboardSnapshot's // outer `repos`/`caveats` shadowed by Stats.repos/Stats.caveats) // produces duplicate field names in the shape — the dashboard // then renders two React children with the same key. depth = 0 for _, rawLine := range strings.Split(inner, "\n") { line := strings.TrimSpace(rawLine) if line == "" || strings.HasPrefix(line, "//") { updateTSBraceDepth(line, &depth) continue } if depth == 0 { emitTSShapeField(line, shape) } updateTSBraceDepth(line, &depth) } if len(shape.Fields) == 0 { return nil } return shape } // emitTSShapeField runs tsFieldRe over one source line and appends a // matching field to shape. Method members (`foo(): R`, `foo(args): R // { ... }`) are skipped because their `(` comes before any `:` — // arrow-typed properties (`foo: (a: A) => R`) still pass since the // `:` is the FIRST punctuator. When the matched type expression is // just an opening `{` (the head of an inline-nested object literal // whose body the brace-depth gate suppressed), we normalise the // recorded type to `{...}` so the dashboard renders a sensible // label instead of a stray brace. func emitTSShapeField(line string, shape *Shape) { colonIdx := strings.Index(line, ":") parenIdx := strings.Index(line, "(") if parenIdx >= 0 && (colonIdx < 0 || parenIdx < colonIdx) { return } m := tsFieldRe.FindStringSubmatch(line) if m == nil { return } nameRaw := strings.Trim(m[1], `'"`) optional := m[2] == "?" typeExpr := strings.TrimSpace(m[3]) comment := "" if len(m) > 4 { comment = truncateComment(m[4]) } if typeExpr == "{" { typeExpr = "{...}" } required := !optional && !tsTypeIsNullable(typeExpr) repeated := tsTypeIsRepeated(typeExpr) shape.Fields = append(shape.Fields, ShapeField{ Name: nameRaw, Type: typeExpr, Required: required, Repeated: repeated, Comment: comment, }) } // updateTSBraceDepth adjusts the running brace depth from one line // of source. Strings and template literals can contain literal // braces, but an interface / type-literal body that uses them // outside an obvious template tag is rare enough we treat them as // real braces; the resulting mis-counts only suppress emit at the // next line, which is a conservative loss vs. the duplicate-key // regression. A `//` comment short-circuits the rest of the line. func updateTSBraceDepth(line string, depth *int) { for i := 0; i < len(line); i++ { switch line[i] { case '{': *depth++ case '}': if *depth > 0 { *depth-- } case '/': if i+1 < len(line) && line[i+1] == '/' { return } } } } // tsTypeIsNullable returns true for union types that include `null` // or `undefined`. Those are optional on the wire even without the // trailing `?` on the field name. func tsTypeIsNullable(t string) bool { parts := strings.Split(t, "|") for _, p := range parts { p = strings.TrimSpace(p) if p == "null" || p == "undefined" { return true } } return false } // tsTypeIsRepeated returns true for `Foo[]`, `Array`, or // `ReadonlyArray`. func tsTypeIsRepeated(t string) bool { t = strings.TrimSpace(t) if strings.HasSuffix(t, "[]") { return true } if strings.HasPrefix(t, "Array<") || strings.HasPrefix(t, "ReadonlyArray<") { return true } return false }