mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
refactor: 0.1.0-beta
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user