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
193 lines
5.7 KiB
Go
193 lines
5.7 KiB
Go
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<Foo> → 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<Foo>`, or
|
|
// `ReadonlyArray<Foo>`.
|
|
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
|
|
}
|