最近百家饭OpenAPI平台的JS API调用代码自动生成功能顺利进展中,进展情况可以关注我们的博客,我们计划先在内部完成“自举”(自己平台开发的功能支持自己的开发……),将百家饭平台自身的前后台交互部分迁移到自己开发的JS代码生成模式上来。

整个过程比较长,分成了golang部分和JavaScript部分,所以我们分篇来介绍,把其中有用的技术点也拎出来说

  1. 第一篇:Golang生成OpenAPI接口文档(Swag工具试用)
  2. 第二篇:Golang OpenAPI工具Swag修正-logrus篇
  3. 第三篇:Golang OpenAPI工具Swag修正-go ast篇
  4. 第四篇:也谈Javascript里的commonjs模块和es6模块

如何对Golang进行注释解析

上一篇我们完成了对swag工具增加日志打印级别的功能修改,我们另外还遇到了一个数据类型无法解析的错误和一个注释获取错误。因此我们继续dig原代码获取修正这些内容的信息。

那首先我们要对swag如何对你的Golalng程序进行注释解析有个初步的了解。我们知道一个程序要被编译成程序,首先要过语法和词法分析这关,语法负责定义一门编程语言的基本书写逻辑,是主谓宾还是主宾谓,if后面是跟{还是直接跟条件,跟总体的顺序逻辑和关系逻辑有关系的都是语法负责,语法把整篇程序的文章结构搞清楚,构建成抽象语法树(Abstract Syntax Tree,AST),抽象语法树一般是数组结构或者链表等存储的树状信息结构,比如“小明吃饭"就梳理成为

[
    {
        Name:"小明", 
        Type:"变量"
    },
    {
        Name:"吃", 
        Type:"函数"
    },
    {
        Name:"饭", 
        Type:"变量"
    }
]

这样的结构基本就具备了计算机进一步处理的能力,计算机再进一步把这个数组进行词法分析,搞清楚小明这个变量和饭这个变量要具体怎么存储,而吃这个函数具体怎么做,那这部分就是词法的部分了。

Golang从1.5版本开始,就内置了go/types package,用于对golang程序的词语法分析,通过这个工具集,我们就具备了分析一个golang程序的基本能力。摘抄官方介绍如下:

The go/types package is a type-checker for Go programs, designed by Robert Griesemer. It became part of Go‘s standard library in Go 1.5. Measured by lines of code and by API surface area, it is one of the most complex packages in Go’s standard library, and using it requires a firm grasp of the structure of Go programs. 

他包含以下的几个包:

  1. go/types
  2. go/constant
  3. go/parser
  4. go/ast
  5. go/scanner
  6. go/token

另外,swag还用到了辅助进行语法解析的另外一个库

golang.org/x/tools/go/loader,这个类似乎大量简化了装载一个go程序的复杂程序,比如swag定义以下函数来加载一个外部库:

func (pkgDefs *PackagesDefinitions) loadExternalPackage(importPath string) error {
	cwd, err := os.Getwd()
	if err != nil {
		return err
	}
    //定义一个装载器和他的工作路径(工作路径其实不需要指定,loader库会自动解析为当前路径
	conf := loader.Config{
		ParserMode: goparser.ParseComments,
		Cwd:        cwd,
	}
    //定义附加的import库
	conf.Import(importPath)
    //进行装载,获得程序体说明
	loaderProgram, err := conf.Load()
	if err != nil {
		return err
	}
    //loaderProgram里就包含了这次解析的所有包名称等信息
	for _, info := range loaderProgram.AllPackages {
		pkgPath := strings.TrimPrefix(info.Pkg.Path(), "vendor/")
		for _, astFile := range info.Files {
			pkgDefs.parseTypesFromFile(astFile, pkgPath, nil)
		}
	}

	return nil
}

因此,如果有对golang有注释解析相关的需求,基本都可以通过基于这个工具来进行二次构建来获得(虽然我非常不愿意看到golang搞得跟java一样,到处都是注解)。

命名引用和非命名引用

那我们遇到的问题是:找不到类型定义

2022/07/06 10:16:17 ParseComment error in file D:\workspace\baijiafan\product_site\ctrl\openapi.go :cannot find type definition: errors.Error

可以看到,我们在注释中为Faiure定义的返回类型errors.Error找不到,但是Success的model.Source是能找到的,我们的import如下:

经过增加日志的形式,我们确认问题是因为errrors库没有名字的原因:

// prior to match named package
	for _, imp := range file.Imports {
		if imp.Name != nil { //errors的引用,这个地方的Name为空
			if imp.Name.Name == pkg {
				return strings.Trim(imp.Path.Value, `"`)
			}

			if imp.Name.Name == "_" {
				hasAnonymousPkg = true
			}

			continue
		}

 原来,解析过的AST,其中Imports的内容,根据golang语言的定义,分为带名称引用和不带名称引用,且和匿名引用还有区别,举例如下:

import (
   codeup.aliyun.com/njzhenwo/baijiafan/base/errors //不带名称引用
   grpc codeup.aliyun.com/njzhenwo/baijiafan/base/grpc //引用的前方有名称
   _ codeup.aliyun.com/njzhenwo/baijiafan/base/proto //匿名引用
)

这三种引用在通过语法解析后,有明显的区别,同时搜索类型的方式也不同:

引用类型import的Name字段import的Path字段类型搜索方法
不带名称引用Null

package名称

装载引用package之后获取包名
带名称引用具体名称package名称用引用名称+'.'+类型名称搜索
匿名引用_package名称直接用类型名称搜索

而目前swag的实现中,没有实现的就是不带名称引用的装载搜索。同时,我们要指出的是golang的package路径的最后一段并不就是包的名称,我们可以从swag的代码中发现这样的代码:

if imp.Name.Name == "_" {
  path := strings.Trim(imp.Path.Value, `"`)
  if fuzzy {
    if matchLastPathPart(path) {
      return path
    }
  } else if pd, ok := pkgDefs.packages[path]; ok && pd.Name == pkg {
    return path
  }
}

实际这个是错误的,很常见的一个例子

gopkg.in/yaml.v3

这个库最后一段是yaml.v3,但是他的包名是yaml

使用loader来解析库名

其实swag里的loader代码我们稍微修改一下就可以完成这项任务:


func (pkgDefs *PackagesDefinitions) parseImportName(importPath string) string {

	conf := loader.Config{
		ParserMode: goparser.PackageClauseOnly,
	}

	conf.Import(importPath)

	loaderProgram, err := conf.Load()
	if err != nil {
		return ""
	}

	for _, info := range loaderProgram.AllPackages {
		pkgPath := strings.TrimPrefix(info.Pkg.Path(), "vendor/")
		if pkgPath == importPath {
			return info.Pkg.Name()
		}
	}

	return ""
}

和前面的装载外部库不同,我们在loader.Config里指定ParserMode为goparser.PackageClauseOnly,这样搜索会在获得包的名称后停止,这样就可以快速的对所有不带名称引用进行搜索了。

另外其实info.Pkg.Path是不会带vendor/前缀的,因为不确定是否老的golang版本有问题,这里就保留了这个TrimPrefix操作。我们还在仓库中修正了另外一个使用问题,这些就不细讲了,我们fork的库在这里,修改已经push到了原仓库,这是commit的链接,还在等待review。这个库我们陆续还会进行一些bug的修正。他的授权协议是mit,所以我们应该也可以在后期把他放到我们的百家饭OpenAPI工具安装包中。但是我觉得至少还需要增加更多的日志打印,最好完成国际化之后再来做这个事情。
 

好了,下一篇终于可以开始讲我们这周关于JS API调用代码自动生成的进展了,关注的朋友可以从目录进去。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐