Files
fun-cli/astscan.go
T
chiyi e29767362a feat: fun-cli v1.0.0 — gen/run/docs 三命令,fun 框架零改动
- fun gen [ts|go]:AST 扫描 BindService* 调用,临时生成 go test 辅助采集,
  经 fun v1.3.7 公开 API 生成,产物与内置 GenCode 字节级一致(回归测试锁定)
- fun run:热更新开发循环,先编译成功再切换,失败保留旧进程
- fun docs:浅色标准文档站——参数表/State 添加字段/请求 JSON 可编辑/
  响应格式化/NDJSON 流式逐行展示,反向代理 /cell 同源无 CORS
- 元数据:TS 产物两遍解析重建(枚举/可空/流式工厂/嵌套引用全支持)
- 纯标准库实现,零第三方依赖
2026-09-19 06:32:45 +08:00

230 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// AST 扫描:在用户模块里找 BindService / BindServiceForGen 调用点,
// 按目录聚合——fun 框架零改动,采集全靠 fun 公开 API
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
type serviceRef struct {
Alias string // &pkg.Svc 的 pkg 别名;同包引用为空
Import string // pkg 对应的导入路径
TypeName string
}
// modulePath 读 go.mod 的 module 行
func modulePath(root string) (string, error) {
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
if err != nil {
return "", fmt.Errorf("找不到 go.mod(%s): %w", root, err)
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "module ") {
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
}
}
return "", fmt.Errorf("go.mod 中没有 module 声明")
}
// findModuleRoot 从 dir 向上找 go.mod
func findModuleRoot(dir string) (string, error) {
abs, err := filepath.Abs(dir)
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(abs, "go.mod")); err == nil {
return abs, nil
}
parent := filepath.Dir(abs)
if parent == abs {
return "", fmt.Errorf("从 %s 向上未找到 go.mod", dir)
}
abs = parent
}
}
var scanSkipDirs = map[string]bool{
".git": true, "vendor": true, "node_modules": true, ".fun-tmp": true, "testdata": true,
}
// scanBindTargets 扫描模块内全部 .go 文件的 BindService* 调用,
// 按目录聚合成待生成辅助测试的包目标(测试文件可访问包内类型,
// 服务定义在 package main 也能采集)
func scanBindTargets(root string) ([]helperTarget, error) {
byDir := map[string]*helperTarget{}
seen := map[string]bool{}
add := func(dir, pkgName string, r serviceRef) {
key := dir + "|" + r.Alias + "|" + r.Import + "|" + r.TypeName
if seen[key] {
return
}
seen[key] = true
t, ok := byDir[dir]
if !ok {
t = &helperTarget{Dir: dir, Pkg: pkgName}
byDir[dir] = t
}
t.Refs = append(t.Refs, r)
}
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
if path != root && (scanSkipDirs[d.Name()] || strings.HasPrefix(d.Name(), ".")) {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
fset := token.NewFileSet()
file, perr := parser.ParseFile(fset, path, nil, 0)
if perr != nil {
return nil // 语法不过的文件(生成物/草稿)跳过
}
// 该文件可见的导入:别名/包名 → 导入路径
imports := map[string]string{}
for _, imp := range file.Imports {
importPath, _ := strconv.Unquote(imp.Path.Value)
alias := filepath.Base(importPath)
if imp.Name != nil {
alias = imp.Name.Name
}
imports[alias] = importPath
}
// 服务列表变量:for _, x := range []any{&A{}, &B{}} / x := []any{...}
// (BindServiceForGen(svc) 循环注册的常见写法)
parsePtrComposite := func(e ast.Expr) (serviceRef, bool) {
u, ok := e.(*ast.UnaryExpr)
if !ok || u.Op != token.AND {
return serviceRef{}, false
}
lit, ok := u.X.(*ast.CompositeLit)
if !ok {
return serviceRef{}, false
}
switch t := lit.Type.(type) {
case *ast.Ident:
return serviceRef{TypeName: t.Name}, true
case *ast.SelectorExpr:
if pkg, ok := t.X.(*ast.Ident); ok {
if importPath, ok := imports[pkg.Name]; ok {
return serviceRef{Alias: pkg.Name, Import: importPath, TypeName: t.Sel.Name}, true
}
}
}
return serviceRef{}, false
}
collectSlice := func(lit *ast.CompositeLit) []serviceRef {
var refs []serviceRef
for _, el := range lit.Elts {
if r, ok := parsePtrComposite(el); ok {
refs = append(refs, r)
}
}
return refs
}
sliceVars := map[string][]serviceRef{}
var collectVars func(n ast.Node) bool
collectVars = func(n ast.Node) bool {
switch st := n.(type) {
case *ast.RangeStmt:
if v, ok := st.Value.(*ast.Ident); ok {
if lit, ok := st.X.(*ast.CompositeLit); ok {
sliceVars[v.Name] = collectSlice(lit)
}
}
case *ast.AssignStmt:
if len(st.Lhs) == 1 {
if id, ok := st.Lhs[0].(*ast.Ident); ok {
if lit, ok := st.Rhs[0].(*ast.CompositeLit); ok {
sliceVars[id.Name] = collectSlice(lit)
}
}
}
}
return true
}
ast.Inspect(file, collectVars)
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
if sel.Sel.Name != "BindService" && sel.Sel.Name != "BindServiceForGen" {
return true
}
if len(call.Args) == 0 {
return true
}
// 参数形如 &pkg.Svc{} / &Svc{}
var typeExpr ast.Expr
if a, ok := call.Args[0].(*ast.UnaryExpr); ok && a.Op == token.AND {
if lit, ok := a.X.(*ast.CompositeLit); ok {
typeExpr = lit.Type
}
}
if ident, ok := call.Args[0].(*ast.Ident); ok {
// BindServiceForGen(svc):循环/切片变量注册
for _, r := range sliceVars[ident.Name] {
add(filepath.Dir(path), file.Name.Name, r)
}
return true
}
if typeExpr == nil {
return true
}
switch t := typeExpr.(type) {
case *ast.Ident:
add(filepath.Dir(path), file.Name.Name, serviceRef{TypeName: t.Name})
case *ast.SelectorExpr:
if pkg, ok := t.X.(*ast.Ident); ok {
if importPath, ok := imports[pkg.Name]; ok {
add(filepath.Dir(path), file.Name.Name, serviceRef{
Alias: pkg.Name, Import: importPath, TypeName: t.Sel.Name,
})
}
}
}
return true
})
return nil
})
if err != nil {
return nil, err
}
dirs := make([]string, 0, len(byDir))
for d := range byDir {
dirs = append(dirs, d)
}
sort.Strings(dirs)
targets := make([]helperTarget, 0, len(dirs))
for _, d := range dirs {
targets = append(targets, *byDir[d])
}
return targets, nil
}