Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

215 lines
6.5 KiB
Go

package contracts
import (
"regexp"
"strings"
)
// goStructBodyRe locates the body of a `type <Name> 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.