refactor: 0.1.0-beta

This commit is contained in:
leokun
2026-08-13 22:01:11 +08:00
parent a3ec2a0dfc
commit 3e7a15017d
401 changed files with 8696 additions and 182189 deletions
+192
View File
@@ -0,0 +1,192 @@
// extractor_test.go 验证压缩 bundle 的字段、别名、服务和合并提取行为。
package main
import "testing"
// TestParseFieldObjectSupportsShorthandType 验证字段类型简写可以解析。
func TestParseFieldObjectSupportsShorthandType(t *testing.T) {
field, err := parseFieldObject(`{no:4,name:"file_not_found",kind:"message",T,oneof:"result"}`)
if err != nil {
t.Fatalf("parse shorthand T: %v", err)
}
if field.T != "T" {
t.Fatalf("parsed shorthand T as %#v, want T", field.T)
}
}
// TestWebpackExportAliasResolvesServiceMessageType 验证 Webpack 导出别名可解析服务消息。
func TestWebpackExportAliasResolvesServiceMessageType(t *testing.T) {
const bundle = `
1:(e,t,n)=>{
n.d(t,{KS:()=>T,_B:()=>r});
var r;
class T {}
T.typeName="agent.v1.AgentClientMessage";
n.proto3.util.setEnumType(r,"agent.v1.DiagnosticSeverity",[]);
},
2:(e,t,n)=>{
var r=n(1);
const service={typeName:"agent.v1.AgentService",methods:{run:{name:"Run",I:r.KS,O:r.KS,kind:n.MethodKind.BiDiStreaming}}};
}`
moduleStarts := buildModuleStarts(bundle)
messages := []Message{{
TypeName: "agent.v1.AgentClientMessage",
VarName: "T",
InternalName: "T",
Package: "agent.v1",
Pos: 35,
ModuleStart: moduleStartForPos(moduleStarts, 35),
}}
enums := []Enum{{
TypeName: "agent.v1.DiagnosticSeverity",
VarName: "r",
Package: "agent.v1",
Pos: 100,
ModuleStart: moduleStartForPos(moduleStarts, 100),
}}
resolver := newTypeResolver(messages, enums, buildAliasIndex(bundle, moduleStarts), buildWebpackExportAliasIndex(bundle, moduleStarts))
resolver.moduleImports = buildModuleImportIndex(bundle, moduleStarts)
typeName, ok := resolver.ResolveTypeName("r.KS", len(bundle)-1, moduleStartForPos(moduleStarts, len(bundle)-1), "agent.v1", "message")
if !ok {
t.Fatal("expected webpack export alias to resolve")
}
if typeName != "agent.v1.AgentClientMessage" {
t.Fatalf("resolved r.KS to %q, want agent.v1.AgentClientMessage", typeName)
}
}
// TestResolverPrefersExpectedKindOverCurrentPackage 验证类型类别优先于当前包候选。
func TestResolverPrefersExpectedKindOverCurrentPackage(t *testing.T) {
resolver := &TypeResolver{bySymbol: map[string][]symbolDef{
"nt": {
{TypeName: "git_forge.v1.GetTagResponse", Kind: "message", Pos: 10, ModuleStart: 1},
{TypeName: "origin.v1.TeamGroupKind", Kind: "enum", Pos: 20, ModuleStart: 1},
},
}}
typeName, ok := resolver.ResolveTypeName("nt", 30, 1, "origin.v1", "message")
if !ok {
t.Fatal("expected cross-package message type to resolve")
}
if typeName != "git_forge.v1.GetTagResponse" {
t.Fatalf("resolved nt to %q, want git_forge.v1.GetTagResponse", typeName)
}
}
// TestModernFactorySyntaxExtractsInAppAdServiceTypes 验证现代工厂语法提取完整服务类型。
func TestModernFactorySyntaxExtractsInAppAdServiceTypes(t *testing.T) {
const bundle = `
42:(e,t,n)=>{
var HasSeenAdRequest=n.makeMessageType("aiserver.v1.HasSeenAdRequest",()=>[{no:1,name:"ad_id",kind:"scalar",T:9}]),
HasSeenAdResponse=n.makeMessageType("aiserver.v1.HasSeenAdResponse",()=>[{no:1,name:"has_seen",kind:"scalar",T:8}]),
MarkAdAsSeenResponse=n.makeMessageType("aiserver.v1.MarkAdAsSeenResponse",[]),
Placement=n.makeEnum("aiserver.v1.InAppAdPlacement",[{no:0,name:"IN_APP_AD_PLACEMENT_UNSPECIFIED",localName:"UNSPECIFIED"}]),
InAppAdService={typeName:"aiserver.v1.InAppAdService",methods:{hasSeenAd:{name:"HasSeenAd",I:HasSeenAdRequest,O:HasSeenAdResponse,kind:n.MethodKind.Unary},markAdAsSeen:{name:"MarkAdAsSeen",I:HasSeenAdRequest,O:MarkAdAsSeenResponse,kind:n.MethodKind.Unary}}};
}`
moduleStarts := buildModuleStarts(bundle)
messages := extractMessages(bundle, moduleStarts)
enums := extractEnums(bundle, moduleStarts)
services := extractServices(bundle, moduleStarts)
if len(messages) != 3 {
t.Fatalf("extracted %d messages, want 3", len(messages))
}
if len(messages[0].Fields) != 1 || messages[0].Fields[0].Name != "ad_id" {
t.Fatalf("unexpected request fields: %#v", messages[0].Fields)
}
if len(enums) != 1 || enums[0].TypeName != "aiserver.v1.InAppAdPlacement" {
t.Fatalf("unexpected enums: %#v", enums)
}
if len(services) != 1 || len(services[0].Methods) != 2 {
t.Fatalf("unexpected services: %#v", services)
}
resolver := newTypeResolver(messages, enums, buildAliasIndex(bundle, moduleStarts), buildWebpackExportAliasIndex(bundle, moduleStarts))
method := services[0].Methods[0]
input, inputOK := resolver.ResolveTypeName(method.InputType, services[0].Pos, services[0].ModuleStart, services[0].Package, "message")
output, outputOK := resolver.ResolveTypeName(method.OutputType, services[0].Pos, services[0].ModuleStart, services[0].Package, "message")
if !inputOK || input != "aiserver.v1.HasSeenAdRequest" {
t.Fatalf("resolved input to %q (ok=%v)", input, inputOK)
}
if !outputOK || output != "aiserver.v1.HasSeenAdResponse" {
t.Fatalf("resolved output to %q (ok=%v)", output, outputOK)
}
}
// TestAssignmentAliasResolvesStandardProtobufType 验证赋值别名解析标准协议类型。
func TestAssignmentAliasResolvesStandardProtobufType(t *testing.T) {
const bundle = `
1:(e,t,n)=>{
var Timestamp=class TimestampMessage extends Base{};
Timestamp.typeName="google.protobuf.Timestamp",Timestamp.fields=n.proto3.util.newFieldList(()=>[]),ua=Timestamp;
var Request=n.makeMessageType("aiserver.v1.Request",()=>[{no:1,name:"created_at",kind:"message",T:ua}]);
}`
moduleStarts := buildModuleStarts(bundle)
messages := extractMessages(bundle, moduleStarts)
resolver := newTypeResolver(messages, nil, buildAliasIndex(bundle, moduleStarts), nil)
typeName, ok := resolver.ResolveTypeName("ua", len(bundle)-1, moduleStartForPos(moduleStarts, len(bundle)-1), "aiserver.v1", "message")
if !ok || typeName != "google.protobuf.Timestamp" {
t.Fatalf("resolved ua to %q (ok=%v), want google.protobuf.Timestamp", typeName, ok)
}
}
// TestDeclarationCoverageReportsUnparsedTypesAndIgnoresGoogleTypes 验证覆盖率忽略标准类型并报告遗漏。
func TestDeclarationCoverageReportsUnparsedTypesAndIgnoresGoogleTypes(t *testing.T) {
const bundle = `
var Request=n.makeMessageType("aiserver.v1.Request",()=>[]);
var Missing=n.makeMessageType("aiserver.v1.Missing",()=>[]);
var Timestamp=n.makeMessageType("google.protobuf.Timestamp",()=>[]);
var Service={typeName:"aiserver.v1.TestService",methods:{}};
`
messages := []Message{{TypeName: "aiserver.v1.Request"}}
services := []Service{{TypeName: "aiserver.v1.TestService"}}
declared, extracted, missing := declarationCoverage(bundle, messages, nil, services)
if declared != 3 || extracted != 2 {
t.Fatalf("coverage=%d/%d, want 2/3", extracted, declared)
}
if len(missing) != 1 || missing[0] != "aiserver.v1.Missing" {
t.Fatalf("unexpected missing declarations: %#v", missing)
}
}
// TestExtractServicesSupportsAnonymousDescriptors 验证匿名服务描述符可以提取。
func TestExtractServicesSupportsAnonymousDescriptors(t *testing.T) {
const bundle = `services.push({typeName:"aiserver.v1.FileSyncService",methods:{sync:{name:"Sync",I:Request,O:Response,kind:n.MethodKind.Unary}}})`
services := extractServices(bundle, nil)
if len(services) != 1 || services[0].TypeName != "aiserver.v1.FileSyncService" {
t.Fatalf("unexpected services: %#v", services)
}
if len(services[0].Methods) != 1 || services[0].Methods[0].Name != "Sync" {
t.Fatalf("unexpected methods: %#v", services[0].Methods)
}
}
// TestMergeMessagesPrefersPrimaryBundleAndKeepsSupplementalTypes 验证合并优先主 bundle 并保留补充类型。
func TestMergeMessagesPrefersPrimaryBundleAndKeepsSupplementalTypes(t *testing.T) {
primary := Message{
TypeName: "aiserver.v1.Shared",
Fields: []Field{{No: 1, Name: "primary", Kind: "scalar", T: 9}},
}
supplemental := Message{
TypeName: "aiserver.v1.Shared",
Fields: []Field{{No: 1, Name: "supplemental", Kind: "scalar", T: 9}},
}
legacy := Message{TypeName: "aiserver.v1.LegacyOnly"}
merged := mergeMessagesByTypeName([]Message{primary, supplemental, legacy})
if len(merged) != 2 {
t.Fatalf("merged %d messages, want 2", len(merged))
}
if merged[0].Fields[0].Name != "primary" {
t.Fatalf("duplicate type did not preserve primary definition: %#v", merged[0])
}
if merged[1].TypeName != "aiserver.v1.LegacyOnly" {
t.Fatalf("supplemental-only type missing: %#v", merged)
}
}
+341
View File
@@ -0,0 +1,341 @@
// generator.go 计算跨包依赖并为各协议包准备完整声明集合。
package main
import (
"fmt"
"os"
)
// generateProtos 按协议包聚合声明并生成对应文件。
func generateProtos(messages []Message, enums []Enum, services []Service, resolver *TypeResolver, outputDir string) {
os.MkdirAll(outputDir, 0755)
// 按协议包聚合声明。
packages := make(map[string]struct {
messages []Message
enums []Enum
services []Service
})
for _, msg := range messages {
pkg := packages[msg.Package]
pkg.messages = append(pkg.messages, msg)
packages[msg.Package] = pkg
}
for _, enum := range enums {
pkg := packages[enum.Package]
pkg.enums = append(pkg.enums, enum)
packages[enum.Package] = pkg
}
for _, svc := range services {
pkg := packages[svc.Package]
pkg.services = append(pkg.services, svc)
packages[svc.Package] = pkg
}
// 建立跨包复制使用的全局类型索引。
allMessages := make(map[string]*Message)
allEnums := make(map[string]*Enum)
for pkgName, pkg := range packages {
if isGooglePkg(pkgName) {
continue
}
for i := range pkg.messages {
msg := &pkg.messages[i]
allMessages[msg.TypeName] = msg
}
for i := range pkg.enums {
enum := &pkg.enums[i]
allEnums[enum.TypeName] = enum
}
}
// 每轮生成前重置已复制类型索引。
copiedTypes = make(map[string]map[string]string)
for pkgName, pkg := range packages {
// Google 标准包直接使用官方协议文件。
if isGooglePkg(pkgName) {
fmt.Printf("跳过: %s (使用官方 proto 文件)\n", pkgName)
continue
}
// 把当前包引用的外部类型复制到本地。
augmentedPkg := copyAllExternalTypes(pkgName, pkg, resolver, allMessages, allEnums)
generateProtoFile(pkgName, augmentedPkg.messages, augmentedPkg.enums, pkg.services, resolver, outputDir)
}
}
// copyAllExternalTypes 递归复制当前包引用的全部外部类型。
func copyAllExternalTypes(pkgName string, pkg struct {
messages []Message
enums []Enum
services []Service
}, resolver *TypeResolver, allMessages map[string]*Message, allEnums map[string]*Enum) struct {
messages []Message
enums []Enum
services []Service
} {
if copiedTypes[pkgName] == nil {
copiedTypes[pkgName] = make(map[string]string)
}
// 建立当前包已有类型集合,并登记本地名称供字段解析使用。
localTypes := make(map[string]bool)
for _, msg := range pkg.messages {
localTypes[msg.ShortName] = true
// 空来源名表示该类型原本就在当前包。
if copiedTypes[pkgName][msg.ShortName] == "" {
copiedTypes[pkgName][msg.ShortName] = "local:" + msg.TypeName
}
}
for _, enum := range pkg.enums {
localTypes[enum.ShortName] = true
if copiedTypes[pkgName][enum.ShortName] == "" {
copiedTypes[pkgName][enum.ShortName] = "local:" + enum.TypeName
}
}
// 结果先保留当前包原始声明。
result := struct {
messages []Message
enums []Enum
services []Service
}{
messages: append([]Message{}, pkg.messages...),
enums: append([]Enum{}, pkg.enums...),
services: pkg.services,
}
totalCopied := 0
// 持续迭代,直到不再发现新的外部依赖。
for round := 1; ; round++ {
// 收集当前消息中的外部类型引用。
neededTypes := make(map[string]bool)
for _, msg := range result.messages {
preferredPkg, _ := parseTypeName(msg.TypeName)
for _, f := range msg.Fields {
collectFieldRefsSimple(f, pkgName, preferredPkg, msg.Pos, msg.ModuleStart, resolver, neededTypes, localTypes)
}
}
for _, svc := range result.services {
for _, m := range svc.Methods {
collectMethodRefsSimple(m.InputType, pkgName, svc.Pos, svc.ModuleStart, resolver, neededTypes, localTypes)
collectMethodRefsSimple(m.OutputType, pkgName, svc.Pos, svc.ModuleStart, resolver, neededTypes, localTypes)
}
}
// 复制本轮新增依赖类型。
copiedThisRound := 0
for typeName := range neededTypes {
refPkg, shortName := parseTypeName(typeName)
if refPkg == pkgName || isGooglePkg(refPkg) {
continue
}
// 已存在于本地时无需重复复制。
if localTypes[shortName] {
continue
}
// 复制消息声明。
if msg, ok := allMessages[typeName]; ok {
msgCopy := *msg
msgCopy.Package = pkgName
// 保留原始完整类型名,用于生成来源注释。
result.messages = append(result.messages, msgCopy)
copiedTypes[pkgName][shortName] = typeName // 保存原始完整类型名。
localTypes[shortName] = true
copiedThisRound++
fmt.Printf(" [%s] 轮%d 复制: %s\n", pkgName, round, typeName)
} else if enum, ok := allEnums[typeName]; ok {
// 复制枚举声明。
enumCopy := *enum
enumCopy.Package = pkgName
result.enums = append(result.enums, enumCopy)
copiedTypes[pkgName][shortName] = typeName
localTypes[shortName] = true
copiedThisRound++
fmt.Printf(" [%s] 轮%d 复制枚举: %s\n", pkgName, round, typeName)
} else {
// 未找到声明时仍登记本地引用,兼容提取结果缺少但 bundle 实际存在的类型。
copiedTypes[pkgName][shortName] = typeName
localTypes[shortName] = true
fmt.Printf(" [%s] 轮%d 警告: 类型未找到 %s,标记为本地引用\n", pkgName, round, typeName)
}
}
totalCopied += copiedThisRound
if copiedThisRound == 0 {
break // 没有新增依赖时结束迭代。
}
if round > 20 {
fmt.Printf(" [%s] 警告: 复制轮次超过20,可能存在问题\n", pkgName)
break
}
}
if totalCopied > 0 {
fmt.Printf(" [%s] 共复制 %d 个外部类型\n", pkgName, totalCopied)
}
return result
}
// collectFieldRefsSimple 收集单个字段直接引用的外部类型。
func collectFieldRefsSimple(f Field, currentPkg string, preferredPkg string, contextPos int, contextModuleStart int, resolver *TypeResolver,
neededTypes map[string]bool, localTypes map[string]bool) {
type refWithKind struct {
ref string
kind string
}
var refs []refWithKind
if f.Kind == "message" || f.Kind == "enum" {
if v, ok := f.T.(string); ok {
refs = append(refs, refWithKind{ref: v, kind: f.Kind})
}
}
if f.Kind == "map" && (f.MapValueKind == "message" || f.MapValueKind == "enum") {
if v, ok := f.MapValueT.(string); ok {
refs = append(refs, refWithKind{ref: v, kind: f.MapValueKind})
}
}
for _, item := range refs {
typeName, ok := resolver.ResolveTypeName(item.ref, contextPos, contextModuleStart, preferredPkg, item.kind)
if !ok {
continue
}
refPkg, shortName := parseTypeName(typeName)
if refPkg == "" || refPkg == currentPkg || isGooglePkg(refPkg) {
continue
}
// 已在当前包中的类型无需收集。
if localTypes[shortName] {
continue
}
neededTypes[typeName] = true
}
}
// collectMethodRefsSimple 收集服务方法输入或输出引用的外部类型。
func collectMethodRefsSimple(ref string, currentPkg string, contextPos int, contextModuleStart int, resolver *TypeResolver,
neededTypes map[string]bool, localTypes map[string]bool) {
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, currentPkg, "message")
if !ok {
return
}
refPkg, shortName := parseTypeName(typeName)
if refPkg == "" || refPkg == currentPkg || isGooglePkg(refPkg) {
return
}
if localTypes[shortName] {
return
}
neededTypes[typeName] = true
}
// copiedTypes 按目标包和短名称记录被复制类型的原始全限定名。
var copiedTypes = make(map[string]map[string]string)
// TypeNode 表示嵌套消息与枚举组成的类型树节点。
type TypeNode struct {
// Name 是当前嵌套层级的类型名。
Name string
// Message 保存当前节点的消息声明。
Message *Message
// Enum 保存当前节点的枚举声明。
Enum *Enum
// Children 保存下一层嵌套类型。
Children map[string]*TypeNode
}
// collectImports 只收集 Google 标准依赖,其余类型会复制到本地。
func collectImports(currentPkg string, messages []Message, services []Service, resolver *TypeResolver) map[string]bool {
imports := make(map[string]bool)
addImport := func(ref string, contextPos int, contextModuleStart int, expectedKind string) {
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, currentPkg, expectedKind)
if !ok {
return
}
refPkg, shortName := parseTypeName(typeName)
// 仅导入 Google 标准类型。
if refPkg == "google.protobuf" {
var importFile string
switch shortName {
case "Struct", "Value", "ListValue", "NullValue":
importFile = "google/protobuf/struct.proto"
case "Timestamp":
importFile = "google/protobuf/timestamp.proto"
case "Duration":
importFile = "google/protobuf/duration.proto"
case "Any":
importFile = "google/protobuf/any.proto"
case "Empty":
importFile = "google/protobuf/empty.proto"
case "FieldMask":
importFile = "google/protobuf/field_mask.proto"
case "BoolValue", "BytesValue", "DoubleValue", "FloatValue",
"Int32Value", "Int64Value", "StringValue", "UInt32Value", "UInt64Value":
importFile = "google/protobuf/wrappers.proto"
default:
importFile = "google/protobuf/descriptor.proto"
}
imports[importFile] = true
} else if refPkg == "google.rpc" {
var importFile string
switch shortName {
case "Status":
importFile = "google/rpc/status.proto"
case "Code":
importFile = "google/rpc/code.proto"
default:
importFile = "google/rpc/status.proto"
}
imports[importFile] = true
}
}
for _, msg := range messages {
for _, f := range msg.Fields {
if f.Kind == "message" || f.Kind == "enum" {
if ref, ok := f.T.(string); ok {
addImport(ref, msg.Pos, msg.ModuleStart, f.Kind)
}
}
// map 值类型也可能引用标准包。
if f.Kind == "map" && (f.MapValueKind == "message" || f.MapValueKind == "enum") {
if ref, ok := f.MapValueT.(string); ok {
addImport(ref, msg.Pos, msg.ModuleStart, f.MapValueKind)
}
}
}
}
for _, svc := range services {
for _, m := range svc.Methods {
addImport(m.InputType, svc.Pos, svc.ModuleStart, "message")
addImport(m.OutputType, svc.Pos, svc.ModuleStart, "message")
}
}
return imports
}
+134
View File
@@ -0,0 +1,134 @@
// main.go 提供协议提取命令的参数解析、输入保护和输出调度。
package main
import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
)
// inputPaths 支持命令行重复传入 bundle 路径。
type inputPaths []string
// String 返回已经登记的输入路径列表。
func (paths *inputPaths) String() string {
return fmt.Sprint([]string(*paths))
}
// Set 追加一个去除空白后的输入路径。
func (paths *inputPaths) Set(value string) error {
*paths = append(*paths, value)
return nil
}
// bailIf 在不可恢复错误时打印信息并退出。
func bailIf(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// findPrettier 定位可用的 prettier 命令。
func findPrettier() (string, error) {
// 尝试常见的 prettier 命令名
names := []string{"prettier", "prettier.cmd", "npx"}
for _, name := range names {
if path, err := exec.LookPath(name); err == nil {
return path, nil
}
}
return "", fmt.Errorf("prettier not found in PATH, please install: npm install -g prettier")
}
// main 解析参数、保护原始输入并执行协议提取。
func main() {
// 命令行参数
var inputs inputPaths
flag.Var(&inputs, "input", "Path to a JS bundle; repeat to merge multiple bundles")
outputDir := flag.String("output", "", "Output directory for proto files (default: ./cursor_proto)")
skipFormat := flag.Bool("skip-format", false, "Skip prettier formatting")
strict := flag.Bool("strict", true, "Fail when extraction validation detects unresolved/placeholder output")
flag.Parse()
// 如果没有 -input 参数,尝试从位置参数获取
if len(inputs) == 0 && flag.NArg() > 0 {
inputs = append(inputs, flag.Args()...)
}
if len(inputs) == 0 {
fmt.Fprintln(os.Stderr, "Usage: ext -input <path-to-js-file> [-input <another-js-file>] [-output <dir>] [-skip-format]")
fmt.Fprintln(os.Stderr, " ext <path-to-js-file>")
fmt.Fprintln(os.Stderr, "\nExample:")
fmt.Fprintln(os.Stderr, " ext -input /path/to/extensionHostProcess.js")
fmt.Fprintln(os.Stderr, " ext C:\\Users\\xxx\\AppData\\Local\\Programs\\cursor\\resources\\app\\out\\vs\\workbench\\api\\node\\extensionHostProcess.js")
os.Exit(1)
}
for _, inputPath := range inputs {
info, err := os.Stat(inputPath)
bailIf(err)
if info.IsDir() {
bailIf(fmt.Errorf("expected %s to be file, is dir", inputPath))
}
}
// 设置输出目录
if *outputDir == "" {
wd, err := os.Getwd()
bailIf(err)
*outputDir = filepath.Join(wd, "cursor_proto")
}
// 复制到临时文件后再格式化,避免修改 Cursor 安装目录。
fmt.Printf("Copying %d source bundle(s) to temp directory...\n", len(inputs))
tempFileNames := make([]string, 0, len(inputs))
for _, inputPath := range inputs {
originalFile, err := os.Open(inputPath)
bailIf(err)
tempFile, err := os.CreateTemp(os.TempDir(), "cursor-source-*.js")
bailIf(err)
_, err = io.Copy(tempFile, originalFile)
bailIf(err)
bailIf(originalFile.Close())
bailIf(tempFile.Close())
tempFileNames = append(tempFileNames, tempFile.Name())
fmt.Printf("Source: %s\n", inputPath)
}
if *skipFormat {
fmt.Println("Skipping formatting (--skip-format)")
} else if prettierBin, err := findPrettier(); err != nil {
fmt.Printf("Warning: %v\n", err)
fmt.Println("Skipping formatting, extraction may be less accurate...")
} else {
fmt.Println("Formatting source bundles (this may take a while)...")
for _, tempFileName := range tempFileNames {
var prettierCmd *exec.Cmd
if filepath.Base(prettierBin) == "npx" {
prettierCmd = exec.Command(prettierBin, "prettier", "--write", tempFileName)
} else {
prettierCmd = exec.Command(prettierBin, "--write", tempFileName)
}
out, formatErr := prettierCmd.CombinedOutput()
if formatErr != nil {
fmt.Printf("Prettier output: %s\n", string(out))
fmt.Println("Warning: formatting failed for one bundle, continuing anyway...")
}
}
}
// 运行提取器
fmt.Println("Extracting Proto definitions...")
SetStrictMode(*strict)
ExtractProtosFromFiles(tempFileNames, *outputDir)
for _, tempFileName := range tempFileNames {
_ = os.Remove(tempFileName)
}
fmt.Printf("\nOutput directory: %s\n", *outputDir)
}
+343
View File
@@ -0,0 +1,343 @@
// messages.go 解析消息声明、字段数组和字段类型信息。
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// extractMessages 从多种 bundle 语法中提取消息声明。
func extractMessages(text string, moduleStarts []int) []Message {
var messages []Message
messageExists := func(typeName, varName string) bool {
for _, existing := range messages {
if existing.TypeName == typeName && existing.VarName == varName {
return true
}
}
return false
}
// 形式一:变量引用继承基类并在类体中声明 typeName 和 fields。
// 先找所有 "变量名 = class 内部类名" 定义
// JS 变量名可以包含 $ 符号,如 B$e, qg 等
// 需要同时捕获外部变量名和内部类名,因为字段引用可能用任一个
classDefRe := regexp.MustCompile(`([\w$]+)\s*=\s*class\s+([\w$]+)\s+extends\s+[\w$.]+\s*\{`)
classMatches := classDefRe.FindAllStringSubmatchIndex(text, -1)
// 从任意包的 this.typeName 字段读取完整类型名。
typeNameRe := regexp.MustCompile(`this\.typeName\s*=\s*"([\w.]+)"`)
// 从 this.fields 的 newFieldList 回调读取字段数组。
fieldsRe := regexp.MustCompile(`this\.fields\s*=\s*\w+(?:\.proto3)?\.util\.newFieldList\s*\(\s*\(\s*\)\s*=>\s*\[`)
for _, classMatch := range classMatches {
varName := text[classMatch[2]:classMatch[3]]
internalName := text[classMatch[4]:classMatch[5]]
classStart := classMatch[0]
// 找到类的结束位置(匹配大括号)
classEnd := findClassEnd(text, classMatch[1]-1)
if classEnd == -1 {
continue
}
classBody := text[classStart:classEnd]
// 在类体内查找 typeName
typeMatch := typeNameRe.FindStringSubmatch(classBody)
if typeMatch == nil {
continue
}
typeName := typeMatch[1]
// 在类体内查找 fields
fieldsMatch := fieldsRe.FindStringIndex(classBody)
if fieldsMatch == nil {
continue
}
// 找到 fields 数组的开始位置
bracketPos := classStart + fieldsMatch[1] - 1
fields := extractFieldArray(text, bracketPos)
pkg, shortName := parseTypeName(typeName)
msg := Message{
TypeName: typeName,
VarName: varName,
InternalName: internalName,
Fields: fields,
Package: pkg,
ShortName: shortName,
Pos: classStart,
ModuleStart: moduleStartForPos(moduleStarts, classStart),
}
messages = append(messages, msg)
}
// 形式二:匹配转译或压缩 bundle 中连续赋值的消息声明。
// 例如 i.runtime=n.proto3,i.typeName="agent.v1.McpArgs",i.fields=n.proto3.util.newFieldList(()=>[{...}])。
assignmentRe := regexp.MustCompile(`([\w$]+)\.typeName\s*=\s*"([\w.]+)"\s*,\s*[\w$]+\.fields\s*=\s*\w+(?:\.\w+)*\.util\.newFieldList\s*\(\s*\(\s*\)\s*=>\s*\[`)
assignmentMatches := assignmentRe.FindAllStringSubmatchIndex(text, -1)
for _, m := range assignmentMatches {
varName := text[m[2]:m[3]]
typeName := text[m[4]:m[5]]
// 跳过已经由类体形式提取的重复消息。
if messageExists(typeName, varName) {
continue
}
// 正则停在左方括号之前,从匹配尾部定位数组起点。
start := m[1] - 1
if start < 0 || start >= len(text) || text[start] != '[' {
continue
}
fields := extractFieldArray(text, start)
pkg, shortName := parseTypeName(typeName)
messages = append(messages, Message{
TypeName: typeName,
VarName: varName,
InternalName: "",
Fields: fields,
Package: pkg,
ShortName: shortName,
Pos: m[0],
ModuleStart: moduleStartForPos(moduleStarts, m[0]),
})
}
// 形式三:匹配现代 @bufbuild/protobuf 工厂调用。
// 例如 Req=A.makeMessageType("aiserver.v1.HasSeenAdRequest",()=>[{...}])。
messageFactoryRe := regexp.MustCompile(`([\w$]+)\s*=\s*[\w$.]+\.makeMessageType\s*\(\s*["']([\w.]+)["']\s*,\s*\(\s*\)\s*=>\s*\[`)
factoryMatches := messageFactoryRe.FindAllStringSubmatchIndex(text, -1)
for _, m := range factoryMatches {
varName := text[m[2]:m[3]]
typeName := text[m[4]:m[5]]
if messageExists(typeName, varName) {
continue
}
bracketStart := m[1] - 1
if bracketStart < 0 || bracketStart >= len(text) || text[bracketStart] != '[' {
continue
}
pkg, shortName := parseTypeName(typeName)
messages = append(messages, Message{
TypeName: typeName,
VarName: varName,
Fields: extractFieldArray(text, bracketStart),
Package: pkg,
ShortName: shortName,
Pos: m[0],
ModuleStart: moduleStartForPos(moduleStarts, m[0]),
})
}
// 空消息直接传字段数组,不使用延迟回调。
// 例如 Res=A.makeMessageType("aiserver.v1.MarkAdAsSeenResponse",[])。
emptyMessageFactoryRe := regexp.MustCompile(`([\w$]+)\s*=\s*[\w$.]+\.makeMessageType\s*\(\s*["']([\w.]+)["']\s*,\s*\[`)
emptyFactoryMatches := emptyMessageFactoryRe.FindAllStringSubmatchIndex(text, -1)
for _, m := range emptyFactoryMatches {
varName := text[m[2]:m[3]]
typeName := text[m[4]:m[5]]
if messageExists(typeName, varName) {
continue
}
bracketStart := m[1] - 1
if bracketStart < 0 || bracketStart >= len(text) || text[bracketStart] != '[' {
continue
}
pkg, shortName := parseTypeName(typeName)
messages = append(messages, Message{
TypeName: typeName,
VarName: varName,
Fields: extractFieldArray(text, bracketStart),
Package: pkg,
ShortName: shortName,
Pos: m[0],
ModuleStart: moduleStartForPos(moduleStarts, m[0]),
})
}
return messages
}
// findClassEnd 查找类定义的配对右花括号。
func findClassEnd(text string, openBrace int) int {
depth := 0
for i := openBrace; i < len(text); i++ {
if text[i] == '{' {
depth++
} else if text[i] == '}' {
depth--
if depth == 0 {
return i + 1
}
}
}
return -1
}
// extractFieldArray 从左方括号位置解析完整字段数组。
func extractFieldArray(text string, start int) []Field {
// 查找字段数组的配对右方括号。
depth := 0
end := start
for i := start; i < len(text); i++ {
if text[i] == '[' {
depth++
} else if text[i] == ']' {
depth--
if depth == 0 {
end = i + 1
break
}
}
}
arrayText := text[start:end]
// 按每个花括号块解析独立字段对象。
var fields []Field
// 依次查找字段对象。
fieldObjects := extractFieldObjects(arrayText)
for _, fieldObj := range fieldObjects {
field, parseErr := parseFieldObject(fieldObj)
if parseErr != nil {
activeDiagnostics.addSkippedField(fieldObj, parseErr)
continue
}
activeDiagnostics.addParsedField()
fields = append(fields, *field)
}
return fields
}
// extractFieldObjects 从数组文本中提取独立字段对象。
func extractFieldObjects(arrayText string) []string {
var objects []string
depth := 0
start := -1
for i := 0; i < len(arrayText); i++ {
if arrayText[i] == '{' {
if depth == 0 {
start = i
}
depth++
} else if arrayText[i] == '}' {
depth--
if depth == 0 && start >= 0 {
objects = append(objects, arrayText[start:i+1])
start = -1
}
}
}
return objects
}
// parseFieldObject 解析包含编号、名称、类型和修饰符的单个字段对象。
func parseFieldObject(obj string) (*Field, error) {
// 提取字段编号。
noMatch := noRe.FindStringSubmatch(obj)
if noMatch == nil {
return nil, errors.New("missing field no")
}
no, _ := strconv.Atoi(noMatch[1])
// 提取字段名称。
nameMatch := nameRe.FindStringSubmatch(obj)
if nameMatch == nil {
return nil, errors.New("missing field name")
}
name := strings.TrimSpace(nameMatch[1])
if !fieldNameRe.MatchString(name) {
return nil, fmt.Errorf("invalid field name: %s", name)
}
// 提取字段类别。
kindMatch := kindRe.FindStringSubmatch(obj)
if kindMatch == nil {
return nil, errors.New("missing field kind")
}
kind := strings.TrimSpace(kindMatch[1])
field := &Field{
No: no,
Name: name,
Kind: kind,
}
// 类型 T 可以是标量编号、变量名或 getEnumType 枚举调用。
// 枚举优先匹配 getEnumType 调用。
if enumMatch := enumTypeRe.FindStringSubmatch(obj); enumMatch != nil {
field.T = enumMatch[1]
} else {
// 其余类型匹配普通 T 属性值。
if tMatch := tRe.FindStringSubmatch(obj); tMatch != nil {
if t, err := strconv.Atoi(tMatch[1]); err == nil {
field.T = t
} else {
field.T = tMatch[1]
}
} else if shorthandTRe.MatchString(obj) {
field.T = "T"
}
}
// 仅在当前字段对象内检查 oneof 分组。
if oneofMatch := oneofRe.FindStringSubmatch(obj); oneofMatch != nil {
candidate := strings.TrimSpace(oneofMatch[1])
if oneofNameRe.MatchString(candidate) {
field.Oneof = candidate
}
}
// 仅在当前字段对象内检查 repeated;压缩 JS 中 !0 表示真。
if repeatedRe.MatchString(obj) {
field.Repeated = true
}
// 仅在当前字段对象内检查 optional。
if optRe.MatchString(obj) {
field.Opt = true
}
// map 字段通过 K 键类型和 V 值描述共同表示。
if field.Kind == "map" {
// 提取 map 键类型。
if keyMatch := keyRe.FindStringSubmatch(obj); keyMatch != nil {
field.MapKey, _ = strconv.Atoi(keyMatch[1])
}
// 提取 map 值类型,兼容属性顺序变化。
if valueMatch := mapValueRe.FindStringSubmatch(obj); valueMatch != nil {
valueObj := valueMatch[1]
if kindMatch := mapValueKRe.FindStringSubmatch(valueObj); kindMatch != nil {
field.MapValueKind = kindMatch[1]
}
if tMatch := mapValueTRe.FindStringSubmatch(valueObj); tMatch != nil {
if t, err := strconv.Atoi(tMatch[1]); err == nil {
field.MapValueT = t
} else {
field.MapValueT = tMatch[1]
}
}
}
}
return field, nil
}
+430
View File
@@ -0,0 +1,430 @@
// modules.go 扫描模块边界、合并声明并执行提取结果校验。
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/desc/protoparse"
)
// moduleStartRe 匹配 Webpack 数字模块的函数起点。
var moduleStartRe = regexp.MustCompile(`(?:^|,)\s*(\d+)\s*:\s*(?:function\s*\(\s*[\w$,\s]*\s*\)|\(\s*[\w$,\s]*\s*\)\s*=>)\s*\{`)
// buildModuleStarts 收集 bundle 内全部模块起始位置。
func buildModuleStarts(text string) []int {
matches := moduleStartRe.FindAllStringSubmatchIndex(text, -1)
starts := make([]int, 0, len(matches))
for _, match := range matches {
starts = append(starts, match[0])
}
return starts
}
// moduleStartForPos 查找指定源码位置所属的模块起点。
func moduleStartForPos(moduleStarts []int, pos int) int {
if len(moduleStarts) == 0 {
return 0
}
index := sort.Search(len(moduleStarts), func(i int) bool {
return moduleStarts[i] > pos
}) - 1
if index < 0 {
return 0
}
return moduleStarts[index]
}
// buildModuleImportIndex 建立模块局部变量到导入模块编号的映射。
func buildModuleImportIndex(text string, moduleStarts []int) map[int]map[string]int {
if len(moduleStarts) == 0 {
return nil
}
moduleMatches := moduleStartRe.FindAllStringSubmatchIndex(text, -1)
moduleStartByID := make(map[string]int, len(moduleMatches))
for _, match := range moduleMatches {
moduleStartByID[text[match[2]:match[3]]] = match[0]
}
importsByModule := make(map[int]map[string]int)
for index, moduleStart := range moduleStarts {
moduleEnd := len(text)
if index+1 < len(moduleStarts) {
moduleEnd = moduleStarts[index+1]
}
body := text[moduleStart:moduleEnd]
for _, match := range moduleImportRe.FindAllStringSubmatch(body, -1) {
targetModuleStart, ok := moduleStartByID[match[2]]
if !ok {
continue
}
if importsByModule[moduleStart] == nil {
importsByModule[moduleStart] = make(map[string]int)
}
importsByModule[moduleStart][match[1]] = targetModuleStart
}
}
return importsByModule
}
// ExtractProtosFromFiles 分别提取各 bundle,规范化类型引用后按全限定名合并。
// 多个 bundle 出现同名声明时优先保留靠前输入。
func ExtractProtosFromFiles(inputFiles []string, outputDir string) {
activeDiagnostics = newExtractionDiagnostics()
defer func() {
activeDiagnostics = nil
}()
var allMessages []Message
var allEnums []Enum
var allServices []Service
for _, inputFile := range inputFiles {
content, err := os.ReadFile(inputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", inputFile, err)
os.Exit(1)
}
text := string(content)
moduleStarts := buildModuleStarts(text)
aliases := buildAliasIndex(text, moduleStarts)
exportAliases := buildWebpackExportAliasIndex(text, moduleStarts)
messages := extractMessages(text, moduleStarts)
enums := extractEnums(text, moduleStarts)
services := extractServices(text, moduleStarts)
declared, extracted, missing := declarationCoverage(text, messages, enums, services)
activeDiagnostics.declaredTypes += declared
activeDiagnostics.extractedTypes += extracted
activeDiagnostics.missingDeclarations = append(activeDiagnostics.missingDeclarations, missing...)
resolver := newTypeResolver(messages, enums, aliases, exportAliases)
resolver.moduleImports = buildModuleImportIndex(text, moduleStarts)
normalizeTypeReferences(messages, services, resolver)
allMessages = append(allMessages, messages...)
allEnums = append(allEnums, enums...)
allServices = append(allServices, services...)
}
messages := mergeMessagesByTypeName(allMessages)
enums := mergeEnumsByTypeName(allEnums)
services := mergeServicesByTypeName(allServices)
for _, msg := range messages {
if len(msg.Fields) == 0 {
activeDiagnostics.emptyMessages = append(activeDiagnostics.emptyMessages, msg.TypeName)
}
}
sort.Strings(activeDiagnostics.missingDeclarations)
activeDiagnostics.missingDeclarations = compactStrings(activeDiagnostics.missingDeclarations)
resolver := newTypeResolver(messages, enums, nil, nil)
generateProtos(messages, enums, services, resolver, outputDir)
validateErr := validateGeneratedProtos(outputDir, activeDiagnostics)
printDiagnosticsSummary(activeDiagnostics)
if strictExtractionValidation && hasValidationFailure(activeDiagnostics, validateErr) {
if validateErr != nil {
fmt.Fprintf(os.Stderr, "Validation failed: %v\n", validateErr)
}
os.Exit(1)
}
if validateErr != nil {
fmt.Fprintf(os.Stderr, "Validation warning: %v\n", validateErr)
}
fmt.Printf("提取完成: %d 个消息, %d 个枚举, %d 个服务\n", len(messages), len(enums), len(services))
}
// normalizeTypeReferences 把字段和方法引用统一转换为全限定类型名。
func normalizeTypeReferences(messages []Message, services []Service, resolver *TypeResolver) {
resolve := func(ref any, contextPos int, moduleStart int, pkg string, kind string) any {
symbol, ok := ref.(string)
if !ok || strings.TrimSpace(symbol) == "" {
return ref
}
if typeName, resolved := resolver.ResolveTypeName(symbol, contextPos, moduleStart, pkg, kind); resolved {
return typeName
}
return ref
}
for messageIndex := range messages {
message := &messages[messageIndex]
for fieldIndex := range message.Fields {
field := &message.Fields[fieldIndex]
if field.Kind == "message" || field.Kind == "enum" {
field.T = resolve(field.T, message.Pos, message.ModuleStart, message.Package, field.Kind)
}
if field.Kind == "map" && (field.MapValueKind == "message" || field.MapValueKind == "enum") {
field.MapValueT = resolve(field.MapValueT, message.Pos, message.ModuleStart, message.Package, field.MapValueKind)
}
}
}
for serviceIndex := range services {
service := &services[serviceIndex]
for methodIndex := range service.Methods {
method := &service.Methods[methodIndex]
if typeName, ok := resolve(method.InputType, service.Pos, service.ModuleStart, service.Package, "message").(string); ok {
method.InputType = typeName
}
if typeName, ok := resolve(method.OutputType, service.Pos, service.ModuleStart, service.Package, "message").(string); ok {
method.OutputType = typeName
}
}
}
}
// mergeMessagesByTypeName 按全限定名合并消息并保留首次声明。
func mergeMessagesByTypeName(messages []Message) []Message {
seen := make(map[string]bool)
merged := make([]Message, 0, len(messages))
for _, message := range messages {
if seen[message.TypeName] {
continue
}
seen[message.TypeName] = true
merged = append(merged, message)
}
return merged
}
// mergeEnumsByTypeName 按全限定名合并枚举并保留首次声明。
func mergeEnumsByTypeName(enums []Enum) []Enum {
seen := make(map[string]bool)
merged := make([]Enum, 0, len(enums))
for _, enum := range enums {
if seen[enum.TypeName] {
continue
}
seen[enum.TypeName] = true
merged = append(merged, enum)
}
return merged
}
// mergeServicesByTypeName 按全限定名合并服务并保留首次声明。
func mergeServicesByTypeName(services []Service) []Service {
seen := make(map[string]bool)
merged := make([]Service, 0, len(services))
for _, service := range services {
if seen[service.TypeName] {
continue
}
seen[service.TypeName] = true
merged = append(merged, service)
}
return merged
}
// compactStrings 清理、去重并排序诊断字符串。
func compactStrings(values []string) []string {
if len(values) == 0 {
return nil
}
compacted := values[:1]
for _, value := range values[1:] {
if value != compacted[len(compacted)-1] {
compacted = append(compacted, value)
}
}
return compacted
}
// hasValidationFailure 判断诊断结果是否达到失败条件。
func hasValidationFailure(diag *extractionDiagnostics, validateErr error) bool {
if validateErr != nil {
return true
}
if diag == nil {
return false
}
if diag.skippedFieldObjects > 0 {
return true
}
if len(diag.unresolvedTypeRefs) > 0 {
return true
}
if len(diag.placeholderHits) > 0 {
return true
}
if len(diag.missingDeclarations) > 0 {
return true
}
return false
}
// printDiagnosticsSummary 输出提取覆盖率和异常样本摘要。
func printDiagnosticsSummary(diag *extractionDiagnostics) {
if diag == nil {
return
}
fmt.Printf(
"诊断汇总: fields %d/%d 解析成功, declarations %d/%d 已提取, skipped=%d, unresolved=%d, placeholders=%d, empty_messages=%d\n",
diag.parsedFieldObjects,
diag.totalFieldObjects,
diag.extractedTypes,
diag.declaredTypes,
diag.skippedFieldObjects,
len(diag.unresolvedTypeRefs),
len(diag.placeholderHits),
len(diag.emptyMessages),
)
if diag.skippedFieldObjects > 0 && len(diag.skippedFieldSamples) > 0 {
fmt.Println("字段解析失败样例:")
for _, sample := range diag.skippedFieldSamples {
fmt.Printf(" - %s\n", sample)
}
}
if len(diag.unresolvedTypeRefs) > 0 {
keys := make([]string, 0, len(diag.unresolvedTypeRefs))
for key := range diag.unresolvedTypeRefs {
keys = append(keys, key)
}
sort.Strings(keys)
fmt.Println("未解析类型引用:")
for _, key := range keys {
fmt.Printf(" - %s (%d)\n", key, diag.unresolvedTypeRefs[key])
}
}
if len(diag.placeholderHits) > 0 {
fmt.Println("占位字段命中:")
for i, hit := range diag.placeholderHits {
if i >= 20 {
fmt.Printf(" - ... and %d more\n", len(diag.placeholderHits)-20)
break
}
fmt.Printf(" - %s\n", hit)
}
}
if len(diag.missingDeclarations) > 0 {
fmt.Println("未提取的 Proto 声明:")
for i, typeName := range diag.missingDeclarations {
if i >= 20 {
fmt.Printf(" - ... and %d more\n", len(diag.missingDeclarations)-20)
break
}
fmt.Printf(" - %s\n", typeName)
}
}
}
// declarationCoverage 比较 bundle 声明数量与实际提取数量。
func declarationCoverage(text string, messages []Message, enums []Enum, services []Service) (int, int, []string) {
declared := make(map[string]bool)
collect := func(re *regexp.Regexp) {
for _, match := range re.FindAllStringSubmatch(text, -1) {
typeName := strings.TrimSpace(match[1])
pkg, _ := parseTypeName(typeName)
if typeName != "" && !isGooglePkg(pkg) {
declared[typeName] = true
}
}
}
collect(typeNameDeclarationRe)
collect(serviceDeclarationRe)
collect(messageDeclarationRe)
collect(enumDeclarationRe)
collect(legacyEnumDeclarationRe)
extracted := make(map[string]bool)
for _, message := range messages {
extracted[message.TypeName] = true
}
for _, enum := range enums {
extracted[enum.TypeName] = true
}
for _, service := range services {
extracted[service.TypeName] = true
}
matched := 0
missing := make([]string, 0)
for typeName := range declared {
if extracted[typeName] {
matched++
continue
}
missing = append(missing, typeName)
}
sort.Strings(missing)
return len(declared), matched, missing
}
// validateGeneratedProtos 检查生成文件语法占位和关键 Agent 结构。
func validateGeneratedProtos(outputDir string, diag *extractionDiagnostics) error {
entries, err := os.ReadDir(outputDir)
if err != nil {
return fmt.Errorf("read output dir failed: %w", err)
}
protoFiles := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if strings.HasSuffix(name, ".proto") {
protoFiles = append(protoFiles, name)
}
}
if len(protoFiles) == 0 {
return errors.New("no generated proto files found")
}
sort.Strings(protoFiles)
for _, file := range protoFiles {
body, readErr := os.ReadFile(filepath.Join(outputDir, file))
if readErr != nil {
return fmt.Errorf("read generated proto failed: %s: %w", file, readErr)
}
lines := strings.Split(string(body), "\n")
for idx, line := range lines {
if placeholderRe.MatchString(line) && diag != nil {
hit := fmt.Sprintf("%s:%d: %s", file, idx+1, strings.TrimSpace(line))
diag.placeholderHits = append(diag.placeholderHits, hit)
}
}
if err := validateRequiredAgentShapes(file, string(body)); err != nil {
return err
}
}
parser := protoparse.Parser{
ImportPaths: []string{outputDir},
LookupImport: desc.LoadFileDescriptor,
}
if _, parseErr := parser.ParseFiles(protoFiles...); parseErr != nil {
return fmt.Errorf("parse generated proto failed: %w", parseErr)
}
return nil
}
// validateRequiredAgentShapes 校验 Agent 流控消息的必要字段形状。
func validateRequiredAgentShapes(file string, body string) error {
if strings.Contains(body, "message ExecClientControlMessage") && !streamCloseRe.MatchString(body) {
return fmt.Errorf("%s: ExecClientControlMessage.stream_close must be ExecClientStreamClose", file)
}
if strings.Contains(body, "message ShellStream") && !shellStdoutRe.MatchString(body) {
return fmt.Errorf("%s: ShellStream.stdout must be ShellStreamStdout", file)
}
return nil
}
+395
View File
@@ -0,0 +1,395 @@
// renderer.go 把协议声明树渲染为稳定的 proto 文本。
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// generateProtoFile 把单个协议包的声明渲染并写入文件。
func generateProtoFile(pkgName string, messages []Message, enums []Enum, services []Service, resolver *TypeResolver, outputDir string) {
// 先收集全部跨包标准依赖。
imports := collectImports(pkgName, messages, services, resolver)
var sb strings.Builder
sb.WriteString(`syntax = "proto3";` + "\n\n")
sb.WriteString(fmt.Sprintf("package %s;\n\n", pkgName))
// 按稳定顺序写入 import。
if len(imports) > 0 {
sortedImports := make([]string, 0, len(imports))
for imp := range imports {
sortedImports = append(sortedImports, imp)
}
sort.Strings(sortedImports)
for _, imp := range sortedImports {
sb.WriteString(fmt.Sprintf("import \"%s\";\n", imp))
}
sb.WriteString("\n")
}
goPackagePath := strings.ReplaceAll(pkgName, ".", "/")
goPackageName := strings.ReplaceAll(pkgName, ".", "")
sb.WriteString(fmt.Sprintf(`option go_package = "github.com/leookun/cursor-byok/cursor-proto/gen/%s;%s";`+"\n\n", goPackagePath, goPackageName))
// 建立嵌套类型树。
root := &TypeNode{Children: make(map[string]*TypeNode)}
for i := range messages {
msg := &messages[i]
path := getNestedPath(msg.ShortName)
insertMessage(root, path, msg)
}
for i := range enums {
enum := &enums[i]
path := getNestedPath(enum.ShortName)
insertEnum(root, path, enum)
}
// 写入全部顶层类型。
writeTypeTree(root, &sb, resolver, 0, pkgName)
// 写入服务声明。
sort.Slice(services, func(i, j int) bool {
return services[i].ShortName < services[j].ShortName
})
for _, svc := range services {
// 写入服务来源注释。
sb.WriteString(fmt.Sprintf("// Source: %s (var: %s)\n", svc.TypeName, svc.VarName))
sb.WriteString(fmt.Sprintf("service %s {\n", svc.ShortName))
for _, m := range svc.Methods {
inputType := resolveMethodType(m.InputType, resolver, pkgName, svc.Pos, svc.ModuleStart)
outputType := resolveMethodType(m.OutputType, resolver, pkgName, svc.Pos, svc.ModuleStart)
switch m.Kind {
case "ServerStreaming":
sb.WriteString(fmt.Sprintf(" rpc %s(%s) returns (stream %s) {}\n", m.Name, inputType, outputType))
case "ClientStreaming":
sb.WriteString(fmt.Sprintf(" rpc %s(stream %s) returns (%s) {}\n", m.Name, inputType, outputType))
case "BiDiStreaming":
sb.WriteString(fmt.Sprintf(" rpc %s(stream %s) returns (stream %s) {}\n", m.Name, inputType, outputType))
default: // 默认为一元调用。
sb.WriteString(fmt.Sprintf(" rpc %s(%s) returns (%s) {}\n", m.Name, inputType, outputType))
}
}
sb.WriteString("}\n\n")
}
// 每个协议包写入扁平输出目录中的单个文件。
fileName := strings.ReplaceAll(pkgName, ".", "_") + ".proto"
filePath := filepath.Join(outputDir, fileName)
os.WriteFile(filePath, []byte(sb.String()), 0644)
fmt.Printf("Generated: %s (%d messages, %d enums, %d services)\n", filePath, len(messages), len(enums), len(services))
}
// resolveMethodType 解析方法消息类型并处理本地复制类型。
func resolveMethodType(ref string, resolver *TypeResolver, currentPkg string, contextPos int, contextModuleStart int) string {
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, currentPkg, "message")
if !ok {
activeDiagnostics.addUnresolvedType("method:" + ref)
return fallbackTypeToken(ref)
}
refPkg, shortName := parseTypeName(typeName)
if refPkg == currentPkg || refPkg == "" {
return shortName
}
// 检查类型是否由其他包复制到当前包。
if copied := copiedTypes[currentPkg]; copied != nil {
if _, isCopied := copied[shortName]; isCopied {
return shortName
}
}
return refPkg + "." + shortName
}
// insertMessage 把消息插入嵌套类型树。
func insertMessage(node *TypeNode, path []string, msg *Message) {
if len(path) == 0 {
return
}
name := path[0]
if node.Children == nil {
node.Children = make(map[string]*TypeNode)
}
child, exists := node.Children[name]
if !exists {
child = &TypeNode{Name: name, Children: make(map[string]*TypeNode)}
node.Children[name] = child
}
if len(path) == 1 {
child.Message = msg
} else {
insertMessage(child, path[1:], msg)
}
}
// insertEnum 把枚举插入嵌套类型树。
func insertEnum(node *TypeNode, path []string, enum *Enum) {
if len(path) == 0 {
return
}
name := path[0]
if node.Children == nil {
node.Children = make(map[string]*TypeNode)
}
child, exists := node.Children[name]
if !exists {
child = &TypeNode{Name: name, Children: make(map[string]*TypeNode)}
node.Children[name] = child
}
if len(path) == 1 {
child.Enum = enum
} else {
insertEnum(child, path[1:], enum)
}
}
// writeTypeTree 按名称稳定输出嵌套消息和枚举。
func writeTypeTree(node *TypeNode, sb *strings.Builder, resolver *TypeResolver, indent int, currentPkg string) {
// 对子节点排序以保证输出稳定。
var names []string
for name := range node.Children {
names = append(names, name)
}
sort.Strings(names)
indentStr := strings.Repeat(" ", indent)
for _, name := range names {
child := node.Children[name]
if child.Enum != nil {
// 检查枚举是否来自其他包。
originalType := ""
if copied := copiedTypes[currentPkg]; copied != nil {
if orig, ok := copied[child.Enum.ShortName]; ok {
originalType = orig
}
}
// 写入枚举来源注释。
if originalType != "" {
sb.WriteString(fmt.Sprintf("%s// Copied from: %s (var: %s)\n", indentStr, originalType, child.Enum.VarName))
} else {
sb.WriteString(fmt.Sprintf("%s// Source: %s (var: %s)\n", indentStr, child.Enum.TypeName, child.Enum.VarName))
}
// 写入枚举声明。
sb.WriteString(fmt.Sprintf("%senum %s {\n", indentStr, name))
for _, v := range child.Enum.Values {
sb.WriteString(fmt.Sprintf("%s %s = %d;\n", indentStr, v.Name, v.No))
}
sb.WriteString(fmt.Sprintf("%s}\n\n", indentStr))
} else if child.Message != nil || len(child.Children) > 0 {
// 写入消息来源注释。
if child.Message != nil {
varInfo := child.Message.VarName
if child.Message.InternalName != "" && child.Message.InternalName != child.Message.VarName {
varInfo = fmt.Sprintf("%s, class: %s", child.Message.VarName, child.Message.InternalName)
}
// 检查消息是否来自其他包。
originalType := ""
if copied := copiedTypes[currentPkg]; copied != nil {
if orig, ok := copied[child.Message.ShortName]; ok {
originalType = orig
}
}
if originalType != "" {
sb.WriteString(fmt.Sprintf("%s// Copied from: %s (var: %s)\n", indentStr, originalType, varInfo))
} else {
sb.WriteString(fmt.Sprintf("%s// Source: %s (var: %s)\n", indentStr, child.Message.TypeName, varInfo))
}
}
// 即使节点只承载嵌套类型,也要写入消息容器。
sb.WriteString(fmt.Sprintf("%smessage %s {\n", indentStr, name))
// 先写入嵌套类型。
writeTypeTree(child, sb, resolver, indent+1, currentPkg)
// 当前节点有消息声明时再写字段。
if child.Message != nil {
writeMessageFields(child.Message, sb, resolver, indent+1)
}
sb.WriteString(fmt.Sprintf("%s}\n\n", indentStr))
}
}
}
// writeMessageFields 输出普通字段和 oneof 分组。
func writeMessageFields(msg *Message, sb *strings.Builder, resolver *TypeResolver, indent int) {
indentStr := strings.Repeat(" ", indent)
// 获取当前消息路径,用于解析相对嵌套类型。
msgPath := msg.ShortName
currentPkg := msg.Package
preferredPkg, _ := parseTypeName(msg.TypeName)
// 按 oneof 分组字段。
oneofGroups := make(map[string][]Field)
var regularFields []Field
for _, f := range msg.Fields {
if f.Oneof != "" {
oneofGroups[f.Oneof] = append(oneofGroups[f.Oneof], f)
} else {
regularFields = append(regularFields, f)
}
}
// 先写普通字段。
for _, f := range regularFields {
fieldType := resolveFieldTypeWithPkg(f, resolver, msgPath, currentPkg, preferredPkg, msg.Pos, msg.ModuleStart)
prefix := ""
if f.Repeated {
prefix = "repeated "
} else if f.Opt {
prefix = "optional "
}
sb.WriteString(fmt.Sprintf("%s%s%s %s = %d;\n", indentStr, prefix, fieldType, f.Name, f.No))
}
// 再写 oneof 字段组。
var oneofNames []string
for name := range oneofGroups {
oneofNames = append(oneofNames, name)
}
sort.Strings(oneofNames)
for _, oneofName := range oneofNames {
fields := oneofGroups[oneofName]
sb.WriteString(fmt.Sprintf("%soneof %s {\n", indentStr, oneofName))
for _, f := range fields {
fieldType := resolveFieldTypeWithPkg(f, resolver, msgPath, currentPkg, preferredPkg, msg.Pos, msg.ModuleStart)
sb.WriteString(fmt.Sprintf("%s %s %s = %d;\n", indentStr, fieldType, f.Name, f.No))
}
sb.WriteString(fmt.Sprintf("%s}\n", indentStr))
}
}
// parseTypeName 从全限定类型名拆出协议包和完整嵌套路径。
func parseTypeName(typeName string) (pkg, shortName string) {
// 优先匹配 xxx.vN.Rest 形式的版本化协议包。
versionRe := regexp.MustCompile(`^([\w.]+\.v\d+)\.(.+)$`)
if match := versionRe.FindStringSubmatch(typeName); match != nil {
return match[1], match[2]
}
// 单独处理 google.protobuf 标准类型。
if strings.HasPrefix(typeName, "google.protobuf.") {
rest := strings.TrimPrefix(typeName, "google.protobuf.")
return "google.protobuf", rest
}
// 单独处理 google.rpc 标准类型。
if strings.HasPrefix(typeName, "google.rpc.") {
rest := strings.TrimPrefix(typeName, "google.rpc.")
return "google.rpc", rest
}
// 无法识别包版本时按最后一个点回退拆分。
parts := strings.Split(typeName, ".")
if len(parts) > 1 {
return strings.Join(parts[:len(parts)-1], "."), parts[len(parts)-1]
}
return "", typeName
}
// getNestedPath 把嵌套类型名拆成逐级路径。
func getNestedPath(shortName string) []string {
return strings.Split(shortName, ".")
}
// resolveFieldTypeWithPkg 结合当前包和父消息路径解析字段类型。
func resolveFieldTypeWithPkg(f Field, resolver *TypeResolver, parentPath string, currentPkg string, preferredPkg string, contextPos int, contextModuleStart int) string {
resolveNamedType := func(ref string, expectedKind string) string {
typeName, ok := resolver.ResolveTypeName(ref, contextPos, contextModuleStart, preferredPkg, expectedKind)
if !ok {
activeDiagnostics.addUnresolvedType(expectedKind + ":" + ref)
return fallbackTypeToken(ref)
}
refPkg, shortName := parseTypeName(typeName)
// 类型位于同一父消息下时使用相对路径。
if parentPath != "" && strings.HasPrefix(shortName, parentPath+".") {
// 例如消息内部将 ConversationMessage.CodeChunk 缩短为 CodeChunk。
return strings.TrimPrefix(shortName, parentPath+".")
}
// 同包类型只使用短名称。
if refPkg == currentPkg || refPkg == "" {
return shortName
}
// 循环依赖中优先使用已经复制到当前包的类型。
if copied := copiedTypes[currentPkg]; copied != nil {
if _, isCopied := copied[shortName]; isCopied {
// 本地存在复制类型时使用短名称。
return shortName
}
}
// 其余跨包引用保留全限定类型名。
return refPkg + "." + shortName
}
if f.Kind == "scalar" {
if t, ok := f.T.(int); ok {
return scalarTypes[t]
}
if t, ok := f.T.(float64); ok {
return scalarTypes[int(t)]
}
}
if f.Kind == "message" || f.Kind == "enum" {
if ref, ok := f.T.(string); ok {
return resolveNamedType(ref, f.Kind)
}
}
if f.Kind == "map" {
// map 字段分别解析键和值类型。
keyType := scalarTypes[f.MapKey]
if keyType == "" {
keyType = "string" // 未知标量默认使用字符串。
}
var valueType string
if f.MapValueKind == "scalar" {
if t, ok := f.MapValueT.(int); ok {
valueType = scalarTypes[t]
} else if t, ok := f.MapValueT.(float64); ok {
valueType = scalarTypes[int(t)]
}
} else if f.MapValueKind == "message" || f.MapValueKind == "enum" {
if ref, ok := f.MapValueT.(string); ok {
valueType = resolveNamedType(ref, f.MapValueKind)
}
}
if valueType == "" {
valueType = "bytes"
}
return fmt.Sprintf("map<%s, %s>", keyType, valueType)
}
return "bytes" // 未识别字段类型时回退为字节串。
}
+423
View File
@@ -0,0 +1,423 @@
// resolver.go 解析压缩 bundle 中的局部符号、模块别名和导出别名。
package main
import (
"regexp"
"sort"
"strings"
)
// newTypeResolver 建立消息、枚举和模块别名的统一索引。
func newTypeResolver(messages []Message, enums []Enum, aliases aliasIndex, exportAliases aliasIndex) *TypeResolver {
resolver := &TypeResolver{
bySymbol: make(map[string][]symbolDef),
byAlias: make(map[string][]symbolDef),
byShort: make(map[string][]symbolDef),
}
add := func(symbol, typeName string, pos int, moduleStart int, kind string) {
symbol = strings.TrimSpace(symbol)
typeName = strings.TrimSpace(typeName)
if symbol == "" || typeName == "" {
return
}
def := symbolDef{TypeName: typeName, Pos: pos, ModuleStart: moduleStart, Kind: kind}
resolver.bySymbol[symbol] = append(resolver.bySymbol[symbol], def)
_, shortName := parseTypeName(typeName)
if shortName != "" {
resolver.byShort[shortName] = append(resolver.byShort[shortName], def)
underscoreAlias := strings.ReplaceAll(shortName, ".", "_")
if underscoreAlias != shortName {
resolver.byShort[underscoreAlias] = append(resolver.byShort[underscoreAlias], def)
}
if idx := strings.LastIndex(shortName, "."); idx > 0 && idx+1 < len(shortName) {
resolver.byShort[shortName[idx+1:]] = append(resolver.byShort[shortName[idx+1:]], def)
}
if idx := strings.LastIndex(underscoreAlias, "_"); idx > 0 && idx+1 < len(underscoreAlias) {
resolver.byShort[underscoreAlias[idx+1:]] = append(resolver.byShort[underscoreAlias[idx+1:]], def)
}
}
}
addAlias := func(symbol, typeName string, pos int, moduleStart int, kind string) {
symbol = strings.TrimSpace(symbol)
typeName = strings.TrimSpace(typeName)
if symbol == "" || typeName == "" {
return
}
resolver.byAlias[symbol] = append(resolver.byAlias[symbol], symbolDef{
TypeName: typeName, Pos: pos, ModuleStart: moduleStart, Kind: kind,
})
}
for _, msg := range messages {
add(msg.VarName, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
if msg.InternalName != "" && msg.InternalName != msg.VarName {
add(msg.InternalName, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
}
for _, alias := range aliasesForSymbols(aliases[msg.ModuleStart], msg.VarName, msg.InternalName) {
addAlias(alias, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
}
}
for _, enum := range enums {
add(enum.VarName, enum.TypeName, enum.Pos, enum.ModuleStart, "enum")
for _, alias := range aliasesForSymbols(aliases[enum.ModuleStart], enum.VarName) {
addAlias(alias, enum.TypeName, enum.Pos, enum.ModuleStart, "enum")
}
}
for _, msg := range messages {
for _, alias := range aliasesForSymbols(exportAliases[msg.ModuleStart], msg.VarName, msg.InternalName) {
addAlias(alias, msg.TypeName, msg.Pos, msg.ModuleStart, "message")
}
}
for _, enum := range enums {
for _, alias := range aliasesForSymbols(exportAliases[enum.ModuleStart], enum.VarName) {
addAlias(alias, enum.TypeName, enum.Pos, enum.ModuleStart, "enum")
}
}
return resolver
}
// buildAliasIndex 提取变量声明和赋值形成的局部别名。
func buildAliasIndex(text string, moduleStarts []int) aliasIndex {
directByModule := make(map[int]map[string]string)
addMatches := func(matches [][]int) {
for _, match := range matches {
alias := strings.TrimSpace(text[match[2]:match[3]])
target := strings.TrimSpace(text[match[4]:match[5]])
if alias == "" || target == "" || alias == target {
continue
}
moduleStart := moduleStartForPos(moduleStarts, match[0])
if directByModule[moduleStart] == nil {
directByModule[moduleStart] = make(map[string]string)
}
directByModule[moduleStart][alias] = target
}
}
addMatches(varAliasRe.FindAllStringSubmatchIndex(text, -1))
addMatches(assignmentAliasRe.FindAllStringSubmatchIndex(text, -1))
resolveRoot := func(direct map[string]string, symbol string) string {
seen := make(map[string]bool)
current := symbol
for {
if seen[current] {
return symbol
}
seen[current] = true
next := direct[current]
if next == "" {
return current
}
current = next
}
}
aliasSets := make(map[int]map[string]map[string]bool)
addAlias := func(moduleStart int, root string, alias string) {
root = strings.TrimSpace(root)
alias = strings.TrimSpace(alias)
if root == "" || alias == "" || root == alias {
return
}
if aliasSets[moduleStart] == nil {
aliasSets[moduleStart] = make(map[string]map[string]bool)
}
if aliasSets[moduleStart][root] == nil {
aliasSets[moduleStart][root] = make(map[string]bool)
}
aliasSets[moduleStart][root][alias] = true
}
for moduleStart, direct := range directByModule {
for alias := range direct {
root := resolveRoot(direct, alias)
addAlias(moduleStart, root, alias)
}
}
if len(aliasSets) == 0 {
return nil
}
aliases := make(aliasIndex, len(aliasSets))
for moduleStart, roots := range aliasSets {
aliases[moduleStart] = make(map[string][]string, len(roots))
for root, set := range roots {
for alias := range set {
aliases[moduleStart][root] = append(aliases[moduleStart][root], alias)
}
sort.Strings(aliases[moduleStart][root])
}
}
return aliases
}
// buildWebpackExportAliasIndex 提取 Webpack 导出表中的符号别名。
func buildWebpackExportAliasIndex(text string, moduleStarts []int) aliasIndex {
aliasSets := make(map[int]map[string]map[string]bool)
addAlias := func(moduleStart int, root string, alias string) {
root = strings.TrimSpace(root)
alias = strings.TrimSpace(alias)
if root == "" || alias == "" || root == alias {
return
}
if aliasSets[moduleStart] == nil {
aliasSets[moduleStart] = make(map[string]map[string]bool)
}
if aliasSets[moduleStart][root] == nil {
aliasSets[moduleStart][root] = make(map[string]bool)
}
aliasSets[moduleStart][root][alias] = true
}
// Webpack 通过 n.d(t, { KS: () => T }) 暴露成员;服务使用 r.KS,消息定义使用局部符号 T。
for _, blockMatch := range webpackExportBlockRe.FindAllStringIndex(text, -1) {
moduleStart := moduleStartForPos(moduleStarts, blockMatch[0])
blockStart := blockMatch[1] - 1
blockEnd := findMatchingBrace(text, blockStart)
if blockEnd == -1 {
continue
}
block := text[blockStart:blockEnd]
for _, entry := range webpackExportEntryRe.FindAllStringSubmatch(block, -1) {
addAlias(moduleStart, entry[2], entry[1])
}
}
if len(aliasSets) == 0 {
return nil
}
aliases := make(aliasIndex, len(aliasSets))
for moduleStart, roots := range aliasSets {
aliases[moduleStart] = make(map[string][]string, len(roots))
for root, set := range roots {
for alias := range set {
aliases[moduleStart][root] = append(aliases[moduleStart][root], alias)
}
sort.Strings(aliases[moduleStart][root])
}
}
return aliases
}
// aliasesForSymbols 返回目标符号集合对应的去重别名。
func aliasesForSymbols(aliases map[string][]string, symbols ...string) []string {
if len(aliases) == 0 {
return nil
}
seen := make(map[string]bool)
var result []string
for _, symbol := range symbols {
for _, alias := range aliases[strings.TrimSpace(symbol)] {
if alias == "" || seen[alias] {
continue
}
seen[alias] = true
result = append(result, alias)
}
}
sort.Strings(result)
return result
}
// looksLikeFullTypeName 判断引用是否已经是全限定协议类型名。
func looksLikeFullTypeName(ref string) bool {
trimmed := strings.TrimSpace(ref)
if strings.HasPrefix(trimmed, "google.protobuf.") || strings.HasPrefix(trimmed, "google.rpc.") {
return true
}
matched, _ := regexp.MatchString(`^[\w.]+\.v\d+\.[\w.]+$`, trimmed)
return matched
}
// pickBestDefinition 按模块、类别、首选包和源码距离选择定义。
func pickBestDefinition(candidates []symbolDef, contextPos int, contextModuleStart int, preferredPkg string, expectedKind string) (symbolDef, bool) {
if len(candidates) == 0 {
return symbolDef{}, false
}
filtered := candidates
if strings.TrimSpace(expectedKind) != "" {
tmp := make([]symbolDef, 0, len(candidates))
for _, item := range candidates {
if item.Kind == expectedKind {
tmp = append(tmp, item)
}
}
if len(tmp) > 0 {
filtered = tmp
}
}
if strings.TrimSpace(preferredPkg) != "" {
tmp := make([]symbolDef, 0, len(filtered))
for _, item := range filtered {
pkg, _ := parseTypeName(item.TypeName)
if pkg == preferredPkg {
tmp = append(tmp, item)
}
}
if len(tmp) > 0 {
filtered = tmp
}
}
if contextModuleStart > 0 {
tmp := make([]symbolDef, 0, len(filtered))
for _, item := range filtered {
if item.ModuleStart == contextModuleStart {
tmp = append(tmp, item)
}
}
if len(tmp) > 0 {
filtered = tmp
}
}
// 选择绝对距离最近的定义,距离相同时优先前向定义。
bestIndex := -1
bestDistance := 0
bestIsFuture := false
for index, item := range filtered {
distance := absInt(item.Pos - contextPos)
isFuture := item.Pos > contextPos
if bestIndex == -1 {
bestIndex = index
bestDistance = distance
bestIsFuture = isFuture
continue
}
if distance < bestDistance {
bestIndex = index
bestDistance = distance
bestIsFuture = isFuture
continue
}
if distance == bestDistance {
// 距离相同时优先当前位置之前的定义。
if bestIsFuture && !isFuture {
bestIndex = index
bestIsFuture = isFuture
}
}
}
if bestIndex < 0 {
return symbolDef{}, false
}
return filtered[bestIndex], true
}
// ResolveTypeName 把局部变量、别名或短名称解析为全限定类型名。
func (resolver *TypeResolver) ResolveTypeName(ref string, contextPos int, contextModuleStart int, preferredPkg string, expectedKind string) (string, bool) {
if resolver == nil {
return "", false
}
trimmed := strings.TrimSpace(ref)
if trimmed == "" {
return "", false
}
if looksLikeFullTypeName(trimmed) {
return trimmed, true
}
resolveBySymbol := func(symbol string, preferSameModule bool) (string, bool) {
candidates := resolver.bySymbol[symbol]
if len(candidates) == 0 {
return "", false
}
moduleStart := 0
if preferSameModule {
moduleStart = contextModuleStart
}
best, ok := pickBestDefinition(candidates, contextPos, moduleStart, preferredPkg, expectedKind)
if !ok {
return "", false
}
return best.TypeName, true
}
resolveByAlias := func(symbol string, targetModuleStart int) (string, bool) {
candidates := resolver.byAlias[symbol]
if len(candidates) == 0 {
return "", false
}
best, ok := pickBestDefinition(candidates, contextPos, targetModuleStart, preferredPkg, expectedKind)
if !ok {
return "", false
}
return best.TypeName, true
}
resolveByShort := func(symbol string, preferSameModule bool) (string, bool) {
candidates := resolver.byShort[symbol]
if len(candidates) == 0 {
return "", false
}
moduleStart := 0
if preferSameModule {
moduleStart = contextModuleStart
}
best, ok := pickBestDefinition(candidates, contextPos, moduleStart, preferredPkg, expectedKind)
if !ok {
return "", false
}
return best.TypeName, true
}
if typeName, ok := resolveBySymbol(trimmed, !strings.Contains(trimmed, ".")); ok {
return typeName, true
}
if typeName, ok := resolveByAlias(trimmed, 0); ok {
return typeName, true
}
if typeName, ok := resolveByShort(trimmed, !strings.Contains(trimmed, ".")); ok {
return typeName, true
}
if strings.Contains(trimmed, ".") {
parts := strings.Split(trimmed, ".")
first := parts[0]
last := parts[len(parts)-1]
targetModuleStart := 0
if imports := resolver.moduleImports[contextModuleStart]; imports != nil {
targetModuleStart = imports[first]
}
if typeName, ok := resolveByAlias(last, targetModuleStart); ok {
return typeName, true
}
if typeName, ok := resolveBySymbol(last, false); ok {
return typeName, true
}
if typeName, ok := resolveByShort(last, false); ok {
return typeName, true
}
if typeName, ok := resolveBySymbol(first, false); ok {
return typeName, true
}
}
return "", false
}
// fallbackTypeToken 从无法解析的引用生成合法类型占位名。
func fallbackTypeToken(ref string) string {
token := strings.TrimSpace(ref)
if token == "" {
return token
}
if strings.Contains(token, ".") {
parts := strings.Split(token, ".")
return parts[len(parts)-1]
}
return token
}
// absInt 返回整数绝对值。
func absInt(value int) int {
if value < 0 {
return -value
}
return value
}
+190
View File
@@ -0,0 +1,190 @@
// services.go 解析枚举、服务方法和压缩对象的配对括号。
package main
import (
"regexp"
"strconv"
)
// extractEnums 从旧式和工厂式声明中提取枚举。
func extractEnums(text string, moduleStarts []int) []Enum {
var enums []Enum
enumExists := func(typeName, varName string) bool {
for _, existing := range enums {
if existing.TypeName == typeName && existing.VarName == varName {
return true
}
}
return false
}
// 匹配任意包中的 setEnumType(XXX, "xxx.v1.EnumName", [...]) 枚举声明。
// JS 变量名可以包含 $ 符号
enumRe := regexp.MustCompile(`setEnumType\s*\(\s*([\w$]+)\s*,\s*"([\w.]+)"\s*,\s*\[`)
matches := enumRe.FindAllStringSubmatchIndex(text, -1)
for _, match := range matches {
varName := text[match[2]:match[3]]
typeName := text[match[4]:match[5]]
// 提取枚举值数组。
bracketStart := match[1] - 1
values := extractEnumValues(text, bracketStart)
pkg, shortName := parseTypeName(typeName)
enum := Enum{
TypeName: typeName,
VarName: varName,
Values: values,
Package: pkg,
ShortName: shortName,
Pos: match[0],
ModuleStart: moduleStartForPos(moduleStarts, match[0]),
}
enums = append(enums, enum)
}
// 匹配现代 @bufbuild/protobuf 工厂形式,例如 Role=A.makeEnum("aiserver.v1.InferenceMessageRole",[{...}])。
enumFactoryRe := regexp.MustCompile(`([\w$]+)\s*=\s*[\w$.]+\.makeEnum\s*\(\s*["']([\w.]+)["']\s*,\s*\[`)
factoryMatches := enumFactoryRe.FindAllStringSubmatchIndex(text, -1)
for _, match := range factoryMatches {
varName := text[match[2]:match[3]]
typeName := text[match[4]:match[5]]
if enumExists(typeName, varName) {
continue
}
bracketStart := match[1] - 1
if bracketStart < 0 || bracketStart >= len(text) || text[bracketStart] != '[' {
continue
}
pkg, shortName := parseTypeName(typeName)
enums = append(enums, Enum{
TypeName: typeName,
VarName: varName,
Values: extractEnumValues(text, bracketStart),
Package: pkg,
ShortName: shortName,
Pos: match[0],
ModuleStart: moduleStartForPos(moduleStarts, match[0]),
})
}
return enums
}
// extractServices 从命名或匿名描述符中提取服务。
func extractServices(text string, moduleStarts []int) []Service {
var services []Service
seenTypeNames := make(map[string]bool)
appendService := func(varName, typeName string, pos, methodsStart int) {
if seenTypeNames[typeName] {
return
}
methodsEnd := findMatchingBrace(text, methodsStart)
if methodsEnd == -1 {
return
}
pkg, shortName := parseTypeName(typeName)
services = append(services, Service{
TypeName: typeName,
VarName: varName,
Methods: extractMethods(text[methodsStart:methodsEnd]),
Package: pkg,
ShortName: shortName,
Pos: pos,
ModuleStart: moduleStartForPos(moduleStarts, pos),
})
seenTypeNames[typeName] = true
}
// 匹配 VarName = { typeName: "xxx.v1.ServiceName", methods: { ... } } 服务对象。
serviceRe := regexp.MustCompile(`([\w$]+)\s*=\s*\{\s*typeName:\s*"([\w.]+)"\s*,\s*methods:\s*\{`)
matches := serviceRe.FindAllStringSubmatchIndex(text, -1)
for _, match := range matches {
varName := text[match[2]:match[3]]
typeName := text[match[4]:match[5]]
appendService(varName, typeName, match[0], match[1]-1)
}
// 部分 bundle 把服务描述符直接放入数组,不预先赋给变量。
anonymousServiceRe := regexp.MustCompile(`\{\s*typeName:\s*["']([\w.]+)["']\s*,\s*methods:\s*\{`)
for _, match := range anonymousServiceRe.FindAllStringSubmatchIndex(text, -1) {
typeName := text[match[2]:match[3]]
appendService("", typeName, match[0], match[1]-1)
}
return services
}
// extractMethods 解析服务对象中的 RPC 方法列表。
func extractMethods(methodsText string) []Method {
var methods []Method
// 匹配包含方法名、输入、输出和调用类型的方法对象。
methodRe := regexp.MustCompile(`\w+:\s*\{\s*name:\s*"([^"]+)"\s*,\s*I:\s*([\w$.]+)\s*,\s*O:\s*([\w$.]+)\s*,\s*kind:\s*[\w$.]+\.(Unary|ServerStreaming|ClientStreaming|BiDiStreaming)`)
matches := methodRe.FindAllStringSubmatch(methodsText, -1)
for _, m := range matches {
method := Method{
Name: m[1],
InputType: m[2],
OutputType: m[3],
Kind: m[4],
}
methods = append(methods, method)
}
return methods
}
// findMatchingBrace 查找花括号块的结束位置。
func findMatchingBrace(text string, start int) int {
depth := 0
for i := start; i < len(text); i++ {
if text[i] == '{' {
depth++
} else if text[i] == '}' {
depth--
if depth == 0 {
return i + 1
}
}
}
return -1
}
// extractEnumValues 从数组起点解析枚举值。
func extractEnumValues(text string, start int) []EnumValue {
// 查找数组的配对结束括号。
depth := 0
end := start
for i := start; i < len(text); i++ {
if text[i] == '[' {
depth++
} else if text[i] == ']' {
depth--
if depth == 0 {
end = i + 1
break
}
}
}
arrayText := text[start:end]
var values []EnumValue
valueRe := regexp.MustCompile(`\{\s*no:\s*(\d+)\s*,\s*name:\s*"([^"]+)"`)
matches := valueRe.FindAllStringSubmatch(arrayText, -1)
for _, m := range matches {
no, _ := strconv.Atoi(m[1])
values = append(values, EnumValue{No: no, Name: m[2]})
}
return values
}
+260
View File
@@ -0,0 +1,260 @@
// types.go 定义协议提取器的领域结构、诊断状态和基础类型映射。
package main
import (
"fmt"
"regexp"
"strings"
)
// isGooglePkg 判断是否为无需重复生成的 Google 标准包。
func isGooglePkg(pkg string) bool {
return pkg == "google.protobuf" || pkg == "google.rpc"
}
// scalarTypes 把运行时标量编号映射为 proto 类型。
var scalarTypes = map[int]string{
1: "double",
2: "float",
3: "int64",
4: "uint64",
5: "int32",
6: "fixed64",
7: "fixed32",
8: "bool",
9: "string",
12: "bytes",
13: "uint32",
15: "sfixed32",
16: "sfixed64",
17: "sint32",
18: "sint64",
}
// strictExtractionValidation 控制校验失败是否终止提取。
var strictExtractionValidation = true
// extractionDiagnostics 汇总字段解析和类型解析诊断。
type extractionDiagnostics struct {
totalFieldObjects int
parsedFieldObjects int
skippedFieldObjects int
skippedFieldSamples []string
unresolvedTypeRefs map[string]int
emptyMessages []string
placeholderHits []string
declaredTypes int
extractedTypes int
missingDeclarations []string
}
// newExtractionDiagnostics 创建一次提取任务的诊断容器。
func newExtractionDiagnostics() *extractionDiagnostics {
return &extractionDiagnostics{
unresolvedTypeRefs: make(map[string]int),
}
}
// addSkippedField 记录未能解析的字段样本和原因。
func (d *extractionDiagnostics) addSkippedField(fieldObject string, reason error) {
if d == nil {
return
}
d.totalFieldObjects++
d.skippedFieldObjects++
if len(d.skippedFieldSamples) < 20 {
trimmed := strings.TrimSpace(fieldObject)
if len(trimmed) > 140 {
trimmed = trimmed[:140] + "..."
}
if reason != nil {
d.skippedFieldSamples = append(d.skippedFieldSamples, fmt.Sprintf("%s | %s", reason.Error(), trimmed))
} else {
d.skippedFieldSamples = append(d.skippedFieldSamples, trimmed)
}
}
}
// addParsedField 累计成功解析的字段数量。
func (d *extractionDiagnostics) addParsedField() {
if d == nil {
return
}
d.totalFieldObjects++
d.parsedFieldObjects++
}
// addUnresolvedType 按引用名称累计类型解析失败次数。
func (d *extractionDiagnostics) addUnresolvedType(ref string) {
if d == nil {
return
}
key := strings.TrimSpace(ref)
if key == "" {
key = "<empty>"
}
d.unresolvedTypeRefs[key]++
}
// SetStrictMode 设置校验失败是否终止提取。
func SetStrictMode(enabled bool) {
strictExtractionValidation = enabled
}
// activeDiagnostics 指向当前提取任务的诊断状态。
var activeDiagnostics *extractionDiagnostics
// 字段解析正则覆盖压缩 bundle 的各类声明形式。
var (
noRe = regexp.MustCompile(`(?:^|[,{]\s*)no:\s*(\d+)`)
nameRe = regexp.MustCompile(`(?:^|[,{]\s*)name:\s*["']([^"']+)["']`)
kindRe = regexp.MustCompile(`(?:^|[,{]\s*)kind:\s*["']([^"']+)["']`)
enumTypeRe = regexp.MustCompile(`[,\s]T:\s*[\w$.]+\.getEnumType\s*\(\s*([\w$.]+)\s*\)`)
tRe = regexp.MustCompile(`[,\s]T:\s*([\w$.]+)`)
oneofRe = regexp.MustCompile(`oneof:\s*["']([^"']+)["']`)
repeatedRe = regexp.MustCompile(`repeated:\s*(!0|true)`)
optRe = regexp.MustCompile(`opt:\s*(!0|true)`)
keyRe = regexp.MustCompile(`[,\s]K:\s*(\d+)`)
mapValueRe = regexp.MustCompile(`V:\s*\{([^}]*)\}`)
mapValueKRe = regexp.MustCompile(`(?:^|[,{]\s*)kind:\s*["'](\w+)["']`)
mapValueTRe = regexp.MustCompile(`[,\s]T:\s*([\w$.]+)`)
shorthandTRe = regexp.MustCompile(`(?:^|[,\{])\s*T\s*(?:[,\}])`)
oneofNameRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
fieldNameRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
placeholderRe = regexp.MustCompile(`^\s*(optional\s+|repeated\s+)?[A-Za-z_][A-Za-z0-9_.<>]*\s+(field_\d+|unknown(?:_[A-Za-z0-9_]+)?)\s*=\s*\d+\s*;`)
varAliasRe = regexp.MustCompile(`\b(?:let|const|var)\s+([\w$]+)\s*=\s*([\w$]+)\s*(?:[,;])`)
assignmentAliasRe = regexp.MustCompile(`(?:^|[;,({])\s*([\w$]+)\s*=\s*([\w$]+)\s*([,;}])`)
webpackExportBlockRe = regexp.MustCompile(`[\w$]+\.d\(\s*[\w$]+\s*,\s*\{`)
webpackExportEntryRe = regexp.MustCompile(`(?:^|[,\{])\s*([\w$]+)\s*:\s*\(\s*\)\s*=>\s*([\w$]+)`)
moduleImportRe = regexp.MustCompile(`(?:\b(?:var|let|const)\s+|,)\s*([\w$]+)\s*=\s*[\w$]+\(\s*(\d+)\s*\)`)
typeNameDeclarationRe = regexp.MustCompile(`(?:\bthis|[\w$]+)\.typeName\s*=\s*["']([\w.]+)["']`)
serviceDeclarationRe = regexp.MustCompile(`\{\s*typeName\s*:\s*["']([\w.]+)["']\s*,\s*methods\s*:`)
messageDeclarationRe = regexp.MustCompile(`\.makeMessageType\s*\(\s*["']([\w.]+)["']`)
enumDeclarationRe = regexp.MustCompile(`\.makeEnum\s*\(\s*["']([\w.]+)["']`)
legacyEnumDeclarationRe = regexp.MustCompile(`\.setEnumType\s*\(\s*[\w$]+\s*,\s*["']([\w.]+)["']`)
streamCloseRe = regexp.MustCompile(`(?s)message\s+ExecClientControlMessage\s*\{.*?ExecClientStreamClose\s+stream_close\s*=\s*1\s*;`)
shellStdoutRe = regexp.MustCompile(`(?s)message\s+ShellStream\s*\{.*?ShellStreamStdout\s+stdout\s*=\s*1\s*;`)
)
// Field 描述一个待渲染的 protobuf 字段。
type Field struct {
// No 是字段编号。
No int `json:"no"`
// Name 是字段名称。
Name string `json:"name"`
// Kind 是标量、消息、枚举或映射类别。
Kind string `json:"kind"`
// T 保存标量编号或消息引用变量。
T any `json:"T"`
// Oneof 是字段所属的互斥分组。
Oneof string `json:"oneof"`
// Repeated 表示字段可以重复。
Repeated bool `json:"repeated"`
// Opt 表示字段为显式可选。
Opt bool `json:"opt"`
// MapKey 是映射键的标量编号。
MapKey int `json:"K"`
// MapValueKind 是映射值的标量或消息类别。
MapValueKind string
// MapValueT 保存映射值的标量编号或消息引用。
MapValueT any
}
// Message 描述提取出的消息及其源码位置。
type Message struct {
// TypeName 是消息的全限定类型名。
TypeName string
// VarName 是 JS 外部变量名。
VarName string
// InternalName 是 JS 内部类名。
InternalName string
// Fields 是消息字段列表。
Fields []Field
// Package 是消息所属协议包。
Package string
// ShortName 是包内嵌套类型名。
ShortName string
// Pos 是消息在 bundle 中的字节位置。
Pos int
// ModuleStart 是消息所在模块的起始位置。
ModuleStart int
}
// Enum 描述提取出的枚举及其源码位置。
type Enum struct {
// TypeName 是枚举的全限定类型名。
TypeName string
// VarName 是枚举对应的 JS 变量名。
VarName string
// Values 是枚举值列表。
Values []EnumValue
// Package 是枚举所属协议包。
Package string
// ShortName 是包内嵌套类型名。
ShortName string
// Pos 是枚举在 bundle 中的字节位置。
Pos int
// ModuleStart 是枚举所在模块的起始位置。
ModuleStart int
}
// EnumValue 描述单个枚举编号和名称。
type EnumValue struct {
// No 是枚举编号。
No int
// Name 是枚举名称。
Name string
}
// Service 描述提取出的服务及其源码位置。
type Service struct {
// TypeName 是服务的全限定类型名。
TypeName string
// VarName 是服务对应的 JS 变量名。
VarName string
// Methods 是服务方法列表。
Methods []Method
// Package 是服务所属协议包。
Package string
// ShortName 是服务包内名称。
ShortName string
// Pos 是服务在 bundle 中的字节位置。
Pos int
// ModuleStart 是服务所在模块的起始位置。
ModuleStart int
}
// Method 描述一个 RPC 方法的输入、输出和流模式。
type Method struct {
// Name 是 RPC 方法名。
Name string
// InputType 是输入消息引用变量。
InputType string
// OutputType 是输出消息引用变量。
OutputType string
// Kind 是一元或不同方向的流式调用类型。
Kind string
}
// symbolDef 保存符号对应的类型、类别和模块位置。
type symbolDef struct {
// TypeName 是符号对应的全限定类型名。
TypeName string
// Pos 是符号定义位置。
Pos int
// Kind 是消息或枚举类别。
Kind string
// ModuleStart 是符号所在模块起点。
ModuleStart int
}
// TypeResolver 通过局部符号、别名和短名称解析协议类型。
type TypeResolver struct {
bySymbol map[string][]symbolDef
byAlias map[string][]symbolDef
byShort map[string][]symbolDef
moduleImports map[int]map[string]int
}
// aliasIndex 按模块和目标符号保存别名集合。
type aliasIndex map[int]map[string][]string