package contracts import ( "regexp" "strings" ) // goStructBodyRe locates the body of a `type struct { ... }` // declaration starting from start_line. We use it only as a quick // shortcut when end_line isn't recorded — normally the parser gives // us an accurate span and we slice that directly. var goStructOpenRe = regexp.MustCompile(`(?s)type\s+\w+\s+struct\s*\{`) // goFieldRe parses one Go struct field line. Capture groups: // // 1: field name (or comma-separated names) // 2: type expression (with optional leading * / [] / map[]) // 3: raw tag body including backticks when present // // Embedded fields (`sync.Mutex`, `io.Reader`) don't match this regex; // we skip them by checking that we got two non-empty groups. var goFieldRe = regexp.MustCompile( "^\\s*([A-Z][A-Za-z0-9_, ]*?)\\s+" + // 1: field name(s) — exported only "(\\*?(?:\\[\\]|map\\[[^\\]]+\\])?[A-Za-z_][\\w.\\[\\]*]*)" + // 2: type expr "(?:\\s+`([^`]*)`)?\\s*(?://\\s*(.*))?$", // 3: tag, 4: comment ) // extractGoShape reads a Go `type X struct { ... }` and returns its // exported field list. Non-exported fields (lower-case first letter) // are dropped because they never appear on the wire. Embedded fields // are skipped — they require cross-file resolution we don't do here. func extractGoShape(src []byte, startLine, endLine int) *Shape { body := sliceBody(src, startLine, endLine) if body == "" { return nil } // Normalise: find the opening brace. Anything before it (package // docs, `type` keyword, the name) is noise. openIdx := strings.Index(body, "{") if openIdx < 0 { // No opening brace in the slice — fall back to brace walk so // types whose end_line == start_line still produce a shape. body = braceBody(src, startLine, 300) openIdx = strings.Index(body, "{") if openIdx < 0 { return nil } } // Balance braces to find the close that matches. 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] shape := &Shape{Kind: "struct"} // Track brace depth across the inner body so we don't hoist // fields of nested anonymous structs into the parent shape. // Lines are scanned at depth 0 (= immediately inside the outer // struct's `{`); when an inline `Stats struct { ... }` 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, "//") { updateGoBraceDepth(line, &depth) continue } if depth == 0 { emitGoStructField(line, shape) } updateGoBraceDepth(line, &depth) } if len(shape.Fields) == 0 { return nil } return shape } // updateGoBraceDepth adjusts the running brace depth from one line // of source. Quoted strings and runes don't carry depth-changing // braces in well-formed Go struct definitions, so a naive count // suffices for the lines we care about. A `//` comment short-circuits // the rest of the line. func updateGoBraceDepth(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 } } } } // emitGoStructField runs goFieldRe over one line and appends every // resulting field (a single decl can declare multiple comma-separated // names) to shape. func emitGoStructField(line string, shape *Shape) { m := goFieldRe.FindStringSubmatch(line) if m == nil { return } names := strings.Split(m[1], ",") typeExpr := strings.TrimSpace(m[2]) tag := m[3] comment := "" if len(m) > 4 { comment = truncateComment(m[4]) } jsonTag, required, repeated, effectiveType := goFieldMetadata(typeExpr, tag) for _, rawName := range names { name := strings.TrimSpace(rawName) if name == "" || !isExportedGo(name) { continue } wireName := name if jsonTag != "" && jsonTag != "-" { wireName = jsonTag } shape.Fields = append(shape.Fields, ShapeField{ Name: wireName, Type: effectiveType, JSONTag: tag, Required: required, Repeated: repeated, Comment: comment, }) } } // goFieldMetadata turns a type expression + raw struct tag into the // pieces ShapeField exposes. // // typeExpr — the declared Go type text // tag — raw tag body without the surrounding backticks // // Returns: wire name from json tag (empty = use field name), required // (true iff tag has no `omitempty` and the type isn't a pointer), // repeated (slice / array type), and the effective type name with // leading `*` / `[]` stripped for diffing friendliness. func goFieldMetadata(typeExpr, tag string) (jsonName string, required, repeated bool, effective string) { // Parse json:"..." out of the raw tag. Tag syntax is // `json:"name,opt1,opt2" xml:"..."` with space separators. for _, entry := range strings.Fields(tag) { if !strings.HasPrefix(entry, "json:") { continue } v := strings.TrimPrefix(entry, "json:") v = strings.Trim(v, `"`) parts := strings.Split(v, ",") jsonName = parts[0] for _, p := range parts[1:] { if p == "omitempty" { // Marked optional. required = false return jsonName, required, strings.HasPrefix(typeExpr, "[]"), strings.TrimPrefix(strings.TrimPrefix(typeExpr, "*"), "[]") } } break } pointer := strings.HasPrefix(typeExpr, "*") repeated = strings.HasPrefix(typeExpr, "[]") effective = strings.TrimPrefix(strings.TrimPrefix(typeExpr, "*"), "[]") // A pointer without `omitempty` is still effectively optional in // most Go JSON handling, so we call it not-required. The explicit // `omitempty` branch above short-circuits before this; here we // infer from type. required = !pointer if jsonName == "-" { // Field is explicitly excluded from JSON. jsonName = "" required = false } return jsonName, required, repeated, effective } func isExportedGo(name string) bool { if name == "" { return false } r := name[0] return r >= 'A' && r <= 'Z' } var _ = goStructOpenRe // reserved for future "find next struct" helper.