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
163 lines
5.5 KiB
Go
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
|
|
}
|