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

223 lines
6.8 KiB
Go

package contracts
import (
"fmt"
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// ThriftExtractor detects Apache Thrift IDL service definitions and
// emits one provider contract per declared service function. The
// consumer side rides on the generated-stub patterns the gRPC
// extractor already recognises (`New<Service>Client(`), so the
// matcher's canonical-name join pairs thrift IDL providers with code
// that calls the generated client.
type ThriftExtractor struct{}
var (
// namespace go shared / namespace java com.example.shared /
// namespace * everything
thriftNamespaceRe = regexp.MustCompile(`(?m)^\s*namespace\s+([\w.*]+)\s+([\w.]+)`)
// service Calculator extends shared.SharedService {
thriftServiceRe = regexp.MustCompile(`(?m)^\s*service\s+(\w+)(?:\s+extends\s+([\w.]+))?\s*\{`)
// One function declaration inside a service block:
// void ping(),
// i32 add(1:i32 num1, 2:i32 num2),
// oneway void zip()
// list<Item> fetch(1: string id) throws (1: NotFound nf);
// Groups: 1 = oneway (or ""), 2 = return type (incl. container
// generics), 3 = function name.
thriftFunctionRe = regexp.MustCompile(`(?m)^\s*(oneway\s+)?([\w.]+(?:\s*<[^>{}]*>)?)\s+(\w+)\s*\(`)
)
// thriftNamespacePreference orders the namespace scopes used to pick
// the single Meta["package"] value when a file declares several. "*"
// applies to every generator, so it wins; after that the order is an
// arbitrary-but-stable language preference.
var thriftNamespacePreference = []string{"*", "go", "java", "py", "python", "js", "rb", "cpp"}
func (e *ThriftExtractor) SupportedLanguages() []string {
// "thrift" matches the parser registry's Language() for .thrift
// files (see internal/parser/languages forest registrations).
return []string{"thrift"}
}
func (e *ThriftExtractor) Extract(filePath string, src []byte, nodes []*graph.Node, edges []*graph.Edge) []Contract {
if !strings.HasSuffix(filePath, ".thrift") {
return nil
}
var out []Contract
text := string(src)
lines := strings.Split(text, "\n")
namespaces := make(map[string]string)
for _, m := range thriftNamespaceRe.FindAllStringSubmatch(text, -1) {
if _, exists := namespaces[m[1]]; !exists {
namespaces[m[1]] = m[2]
}
}
pkg := pickThriftNamespace(namespaces)
for _, sMatch := range thriftServiceRe.FindAllStringSubmatchIndex(text, -1) {
serviceName := text[sMatch[2]:sMatch[3]]
// sMatch[1] points just past the `{`; bound the function scan
// to this service's block so sibling services stay separate.
blockEnd := matchCloseBrace(text, sMatch[1])
if blockEnd < 0 {
blockEnd = len(text)
}
blockStart := sMatch[1]
block := text[blockStart:blockEnd]
qualService := serviceName
if pkg != "" {
qualService = pkg + "." + serviceName
}
for _, fm := range thriftFunctionRe.FindAllStringSubmatchIndex(block, -1) {
oneway := fm[2] >= 0
retType := strings.TrimSpace(block[fm[4]:fm[5]])
name := block[fm[6]:fm[7]]
// Filter declarations the loose line regex can't tell
// apart from functions: keyword-led lines are struct /
// enum / typedef bodies that only appear in malformed
// files, but cheap to guard anyway.
if isThriftKeyword(retType) && retType != "void" {
continue
}
absOffset := blockStart + fm[0]
line := lineNumber(lines, absOffset)
meta := map[string]any{
"service": serviceName,
"method": name,
"canonical": qualService + "/" + name,
}
if pkg != "" {
meta["package"] = pkg
}
if len(namespaces) > 0 {
meta["namespaces"] = namespaces
}
if oneway {
meta["oneway"] = true
}
if retType != "void" {
meta["response_type"] = retType
meta["schema_source"] = "extracted"
} else {
meta["schema_source"] = "none"
}
// fm[7] points at the function name's end; the `(`
// follows after optional whitespace. Balance-scan the
// argument list and record each field as "name:type".
if args := thriftArgList(block, fm[7]); len(args) > 0 {
meta["args"] = args
}
out = append(out, Contract{
ID: fmt.Sprintf("thrift::%s::%s", serviceName, name),
Type: ContractThrift,
Role: RoleProvider,
FilePath: filePath,
Line: line,
Meta: meta,
Confidence: 0.95,
})
}
}
return out
}
// pickThriftNamespace selects the Meta["package"] value from the
// declared namespaces, preferring the generator-agnostic "*" scope,
// then a stable per-language order, then any remaining scope.
func pickThriftNamespace(namespaces map[string]string) string {
for _, scope := range thriftNamespacePreference {
if ns, ok := namespaces[scope]; ok {
return ns
}
}
for _, ns := range namespaces {
return ns
}
return ""
}
// thriftArgList parses the parenthesised field list that starts after
// nameEnd (the byte offset just past the function name) and returns
// one "name:type" entry per field. Thrift fields look like
// `1: required i32 num1` — the numeric id and requiredness qualifier
// are stripped, the declared type and name kept.
func thriftArgList(block string, nameEnd int) []string {
open := strings.Index(block[nameEnd:], "(")
if open < 0 {
return nil
}
openEnd := nameEnd + open + 1
closeAt := matchCloseParen(block, openEnd)
if closeAt < 0 {
return nil
}
var out []string
for _, raw := range splitTopLevelArgs(block[openEnd:closeAt]) {
field := strings.TrimSpace(raw)
if field == "" {
continue
}
// Strip the leading `N:` field id.
if colon := strings.Index(field, ":"); colon >= 0 && isThriftFieldID(field[:colon]) {
field = strings.TrimSpace(field[colon+1:])
}
// Strip requiredness qualifiers.
for _, q := range []string{"required ", "optional "} {
field = strings.TrimPrefix(field, q)
}
// Drop a default value (`= 42`).
if eq := strings.Index(field, "="); eq >= 0 {
field = strings.TrimSpace(field[:eq])
}
// What remains is `<type> <name>` with the type possibly
// containing generics and spaces (map<i32, string>). The name
// is the final identifier.
if sp := strings.LastIndexAny(field, " \t>"); sp >= 0 && sp+1 < len(field) {
name := strings.TrimSpace(field[sp+1:])
typ := strings.TrimSpace(field[:sp+1])
if name != "" && typ != "" {
out = append(out, name+":"+typ)
continue
}
}
out = append(out, field)
}
return out
}
// isThriftFieldID reports whether s is a numeric thrift field id (the
// `1` in `1: i32 num1`).
func isThriftFieldID(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// isThriftKeyword reports whether the token is a thrift declaration
// keyword that cannot be a function return type.
func isThriftKeyword(s string) bool {
switch s {
case "struct", "enum", "union", "exception", "typedef", "const",
"service", "include", "namespace", "throws", "void":
return true
}
return false
}