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

163 lines
5.5 KiB
Go

package contracts
import (
"encoding/gob"
"strings"
)
// Shape is stored in graph.Node.Meta (a map[string]any), which gets
// gob-encoded into the daemon snapshot. Gob refuses to encode a concrete
// type reached through an interface slot unless that type is registered,
// so the first node with a shape would abort the snapshot write with
// "type not registered for interface: contracts.Shape". Register the
// pointer form because that's what ExtractShape returns and what the
// indexer writes into Meta.
func init() {
gob.Register((*Shape)(nil))
}
// Shape captures the externally-visible structure of a type that's
// referenced as a contract's request / response body. It's attached to
// the type's graph node (via `Meta["shape"]`) so downstream tooling —
// the UI schema pane, a `contracts validate` pass, change-impact
// analysis — can diff field lists without re-reading source.
//
// We serialise the shape explicitly rather than relying on whoever
// reads the node to re-parse the file, because field-level diffing
// needs to work even when only one side of a cross-repo contract is
// still indexed.
type Shape struct {
// Kind is the syntactic flavour of the definition:
// "struct" (Go), "interface" | "type" (TS), "class" (Python),
// "class" (Java/Kotlin), "message" (proto).
Kind string `json:"kind"`
Fields []ShapeField `json:"fields"`
// Notes carries extractor diagnostics that aren't a hard error
// but tell a reader why the shape might be incomplete.
Notes []string `json:"notes,omitempty"`
}
// ShapeField is one field on a type.
type ShapeField struct {
// Name is the wire name (serialised form). For Go this is the JSON
// tag when present, else the struct field name lower-cased? No —
// we preserve the declared field name and put the wire tag in
// JSONTag. Consumers choose which to display.
Name string `json:"name"`
// Type is a textual type expression — "string", "int64",
// "[]User", "Profile", "Optional[int]", "List<User>". May be a
// bare identifier (upgradeable to a symbol ID later) or a
// compound expression.
Type string `json:"type"`
// JSONTag is the `json:"..."` value on Go struct tags,
// `@JsonProperty("...")` on Java / Jackson, `alias="..."` on
// Pydantic, `json_name = "..."` on proto. Empty when none.
JSONTag string `json:"json_tag,omitempty"`
// Required is true when the field must be present on the wire.
// Go: tag has no `omitempty` AND field is not a pointer.
// TS: no trailing `?` and not unioned with `null` / `undefined`.
// Python: no default value AND type isn't Optional / | None.
// Java: no `@Nullable` / `Optional<T>`.
// Proto3: default is optional; `required` synthesises the flag.
Required bool `json:"required"`
// Repeated is true for list / array / slice types.
Repeated bool `json:"repeated,omitempty"`
// Comment is the adjacent docstring / line comment when present.
// We keep at most one line; long comments get truncated so the
// graph payload stays compact.
Comment string `json:"comment,omitempty"`
}
// ExtractShape dispatches to the correct language-specific extractor
// based on the type's file extension. Returns nil when the file isn't
// a supported language or the extractor couldn't find any fields.
// Callers attach the returned Shape to the type node's meta.
func ExtractShape(filePath string, src []byte, startLine, endLine int) *Shape {
lang := detectLanguage(filePath)
switch lang {
case "go":
return extractGoShape(src, startLine, endLine)
case "typescript", "javascript":
return extractTSShape(src, startLine, endLine)
case "python":
return extractPythonShape(src, startLine, endLine)
case "java", "kotlin":
return extractJavaShape(src, startLine, endLine)
case "dart":
return extractDartShape(src, startLine, endLine)
case "rust":
return extractRustShape(src, startLine, endLine)
case "csharp":
return extractCSharpShape(src, startLine, endLine)
}
if strings.HasSuffix(filePath, ".proto") {
return extractProtoShape(src, startLine, endLine)
}
return nil
}
// sliceBody returns the source lines from start_line to end_line
// (1-based, inclusive). When end_line is zero or less than start the
// parser didn't record a span — caller should use a brace-walked
// fallback or skip.
func sliceBody(src []byte, startLine, endLine int) string {
if startLine <= 0 {
return ""
}
lines := strings.Split(string(src), "\n")
if startLine > len(lines) {
return ""
}
end := endLine
if end <= 0 || end > len(lines) {
end = len(lines)
}
if end < startLine {
end = startLine
}
return strings.Join(lines[startLine-1:end], "\n")
}
// braceBody is the fallback when a type node's end_line isn't
// recorded. Walk from start_line forward, count braces, and stop when
// the first one closes.
func braceBody(src []byte, startLine int, maxLines int) string {
lines := strings.Split(string(src), "\n")
if startLine <= 0 || startLine > len(lines) {
return ""
}
depth := 0
opened := false
end := startLine
limit := startLine + maxLines
if limit > len(lines) {
limit = len(lines)
}
for i := startLine - 1; i < limit; i++ {
line := lines[i]
for _, ch := range line {
if ch == '{' {
depth++
opened = true
} else if ch == '}' && opened {
depth--
}
}
end = i + 1
if opened && depth <= 0 {
break
}
}
return strings.Join(lines[startLine-1:end], "\n")
}
// truncateComment keeps a field's doc comment to a reasonable size so
// the shape payload doesn't balloon on heavily-annotated types.
func truncateComment(s string) string {
s = strings.TrimSpace(s)
if len(s) > 120 {
s = s[:117] + "..."
}
return s
}