├── .gitattributes ├── .gitignore ├── README.md ├── docu ├── abs_test.go ├── comments.go ├── comments_test.go ├── diff.go ├── differ.go ├── docu.go ├── env.go ├── filter.go ├── index.go ├── list.go ├── lit.go ├── markdown.go ├── merge.go ├── merge_test.go ├── performance_test.go ├── printer.go ├── printer_test.go ├── replace.go ├── replace_test.go ├── target.go ├── target_test.go ├── template.go └── testdata │ ├── code_origin.text │ ├── merge_origin_trans.text │ ├── origin.go │ ├── replace_source.text │ ├── replace_source_merge_origin_trans.text │ └── trans.go └── main.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | *. text eol=lf 3 | .* text eol=lf 4 | *.go text eol=lf 5 | *.html text eol=lf 6 | *.tmpl text eol=lf 7 | *.yml text eol=lf 8 | *.toml text eol=lf 9 | *.md text eol=lf 10 | *.json text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | pkg 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoDocu 2 | 3 | Godocu 基于 [docu][] 实现的指令行工具, 从 Go 源码生成文档. 4 | 5 | 功能: 6 | 7 | - 80 列换行, 支持多字节字符 8 | - 若原注释已经符合 80 列换行, 保持不变. 9 | - 缩进使用 tab(width = 4) 10 | - 换行使用 "\n" 11 | - 多平台文档只提取 linux, amd64 的组合 12 | - 可提取执行包文档, 测试包文档, 非导出符号文档 13 | - 遍历目录 14 | - 过滤掉同目录多包 15 | - 生成文档格式含 Go 源码风格, Markdown 风格, 支持模板 16 | - 生成文档概要清单 17 | - 合并生成双语文档 18 | - 合并两份双语文档中的翻译成果 19 | - 比较两份文档差异 20 | - 比较两个包目录结构差异 21 | 22 | 该工具在 Golang 官方包下测试通过, 非官方包请核对输出结果. 23 | 24 | 文件名命名风格: 25 | 26 | Godocu 文档文件名由前缀 "doc"|"main"|"test" 和语言后缀 "_lang" 以及扩展名组成. 27 | 28 | 扩展名 29 | 30 | - `code`,`merge`,`replace` 指令输出扩展名为 ".go". 31 | - `tmpl` 指令扩展名由模板决定, 比如 ".md" 32 | 33 | 丢弃下列尾注释 34 | 35 | ```go 36 | const ( // this comment is discarded 37 | // ... 38 | ) 39 | 40 | type T struct { // this comment is discarded 41 | // ... 42 | } 43 | ``` 44 | 45 | 格式化差异 46 | 47 | go fmt 格式化后为 48 | 49 | ```go 50 | const Docu = 1 // comment Docu 51 | const Hi = 1 // comment Hi 52 | ``` 53 | 54 | godocu 格式化后为 55 | 56 | ```go 57 | const Docu = 1 // comment Docu 58 | const Hi = 1 // comment Hi 59 | ``` 60 | 61 | # Install 62 | 63 | ``` 64 | go get github.com/golang-china/godocu 65 | ``` 66 | 67 | # Usage 68 | 69 | ``` 70 | Usage: 71 | 72 | godocu command [arguments] source [target] 73 | 74 | The commands are: 75 | 76 | diff compare the source and target, all difference output 77 | first compare the source and target, the first difference output 78 | tree compare different directory structure of the source and target 79 | code prints a formatted string to target as Go source code 80 | tmpl prints documentation from template 81 | list generate godocu style documents list 82 | merge merge source doc to target 83 | replace replace the target untranslated section in source translated section 84 | 85 | The source are: 86 | 87 | package import path or absolute path 88 | the path to a Go source file 89 | 90 | The target are: 91 | 92 | the directory as an absolute base path for compare or prints 93 | 94 | The arguments are: 95 | 96 | -file string 97 | template file for tmpl 98 | -gopath string 99 | specifies GOPATH (default $GOPATH) 100 | -goroot string 101 | specifies GOROOT (default $GOROOT) 102 | -lang string 103 | the lang pattern for the output file, form like en or zh_CN 104 | -p string 105 | package filtering, "package"|"main"|"test" (default "package") 106 | -u 107 | show unexported symbols as well as exported 108 | ``` 109 | 110 | # source 111 | 112 | source 必选, 用于计算源文件绝对路径, 和 import path. 113 | source 可以是 import path 或绝对路径表示的目录或文件. 114 | 115 | 如果是 import path, 先在 `GOROOT/src`, `GOPATH/src` 下查找并计算出绝对路径. 116 | 因 go 源文件绝对路径比较规律, Godocu 通过绝对路径计算出 import path. 117 | 118 | 在 source 尾部加 `...` 表示遍历子目录. 119 | 若 source 值为 `...` 特指全部官方包, 即遍历 `GOROOT/src` 下的所有包. 120 | 121 | 遍历目录时 Godocu 参照 Go 命名习惯, 忽略 `testdata`, `vendor` 之类的目录. 122 | 123 | 详情参见相关指令以及 [Example](#example). 124 | 125 | # target 126 | 127 | target 除 `list` 指令外都表示基础目标路径, 配合 souce 计算出目标路径. 128 | 129 | Godocu 要求某个包的目录结构在 source 和 target 下是相同的. 130 | 131 | 方便起见, target 值为 "--" 表示输出到 source 计算得到的原包目录. 132 | 133 | 对于 `code`, `list`,`tmpl` 指令, target 可选, 缺省输出到 Stdout. 134 | 135 | 对于 `diff`, `first`, `tree` 指令, target 必选, 结果输出到 Stdout. 136 | 137 | 对于 `merge',`replace` 指令, target 必选. 138 | 139 | *安全起见, 只有显示指定 `lang` 参数, 才会生成或覆盖目标文件, 否则输出到 Stdout* 140 | 141 | 详情参见相关指令以及 [Example](#example). 142 | 143 | # lang 144 | 145 | 参数 `lang` 指定目标文件名后缀, 格式为 lang 或 lang_ISOCountryCode. 146 | 即 lang 部分为小写, ISOCountryCode 部分为大写. 147 | 148 | 辅助函数 `docu.LangNormal` 提供规范化处理. 149 | 150 | 方便起见, 未指定 `lang` 时, Godocu 尝试从 target 下首个匹配的包提取 `lang`. 151 | 152 | *提示: 同一目录下的翻译文档应具有相同的 lang* 153 | 154 | 详情参见相关指令. 155 | 156 | # package_filtering 157 | 158 | 参数 'p' 用于过滤包, 可选值为 "package","main","test" 之一. 缺省为 "package". 159 | 160 | 即: Godocu 每次只处理一种类别的包: 库,可执行包或测试包. 161 | 162 | # unexported 163 | 164 | 参数 'u' 允许文档包含顶级非导出声明. 165 | 该参数只对 `diff`, `first`, `code`, `tmpl` 指令有效. 166 | 167 | 比如 `builtin` 包的声明多是非导出的, 但在文档中是不可或缺的. 168 | 169 | Godocu 的非导出优先策略是: 170 | 171 | 1. 如果使用了 'u' 参数, 包含全部非导出声明 172 | 2. 如果目标已存在且有 Godocu 风格 ".go" 文件, 其中的非导出声明被保留 173 | 3. 否则不输出非导出声明 174 | 175 | 176 | # goroot 177 | 178 | 仅当 source 为 import path 时, 参数 `goroot`,`gopath` 用于计算绝对路径. 179 | 180 | # file 181 | 182 | 参数 'file' 表示外部文件, 目前仅为 `tmpl` 指令指定外部模板文件. 183 | 184 | # Merge 185 | 186 | 指令 `merge` 合并 source 文档到 target 中相同顶级声明的文档之前, 生成翻译文档. 187 | 188 | *注意: 输出结果保持 source 的代码结构, merge 在整个工具链中非常重要* 189 | 190 | 翻译文档生是双(语)文档, 但 merge 不分析文档所用的语言. 191 | 翻译文档代码结构可能和原文档不同, 比如:原文档中用了分组, 翻译文档没用. 192 | 使用 merge 可保证文档结构风格和原代码一致. 193 | 194 | - source 可以是源码或包文档, 事实上使用源码具有现实意义. 195 | - target 可以是源码或包文档. 196 | - 结果总是使用 source 的 import. 197 | - 如果声明的文档一样, 不追加, 即只有一份文档. 198 | - 尾注释翻译格式约定: // origin comment // trans comment 199 | - 如果 target 中是尾注释翻译, 保留该翻译, 否则使用 source 的尾注释. 200 | - 指定 `lang` 参数才生成或覆盖 target, 否则仅向 stdout 打印结果. 201 | - 最终结果 source 中已被删除的声明会被剔除, 新声明会出现. 202 | 203 | 合并 `builtin` 包文档到 [translations][]. 204 | 205 | ```shell 206 | $ godocu merge builtin /path/to/github.com/golang-china/golangdoc.translations/src 207 | ``` 208 | 209 | 遍历所有官方包文档合并到 [translations][]. 210 | 211 | ```shell 212 | $ godocu merge ... /path/to/github.com/golang-china/golangdoc.translations/src 213 | ``` 214 | 215 | # Code 216 | 217 | 指令 `code` 输出 ".go" 格式单文档. 218 | 219 | 如果指定了 target 要求参数 `lang` 非空. 220 | 221 | 输出 `builtin` 包文档, 显然要加参数 'u' 222 | 223 | ```shell 224 | $ godocu code builtin -u 225 | ``` 226 | 227 | 如你所见, Godocu 支持 "-" 开头的参数在任意位置出现. 228 | 229 | # Tmpl 230 | 231 | 指令 `tmpl` 支持模板输出, 参数 'file' 指定模板文件, 缺省为内置的 Markdown 模板. 232 | 233 | # Tree 234 | 235 | 指令 `tree` 遍历比较输出 sourec, target 目录结构差异. 236 | 237 | *注意: 参数 lang, p 在该指令下无效* 238 | 239 | 该指令总是遍历目录, source 无需加 "..." 240 | 241 | 遍历比较当前版本 1.6.2 和老版本的目录差异: 242 | 243 | ```shell 244 | $ godocu tree ... /usr/local/Cellar/go/1.5.2/libexec/src 245 | ``` 246 | 247 | 输出: 248 | 249 | ``` 250 | source: /usr/local/Cellar/go/1.6.2/libexec/src 251 | target: /usr/local/Cellar/go/1.5.2/libexec/src 252 | 253 | source target path 254 | path none cmd/compile/internal/mips64 255 | path none cmd/internal/obj/mips 256 | path none cmd/internal/unvendor 257 | path none cmd/internal/unvendor/golang.org 258 | path none cmd/internal/unvendor/golang.org/x 259 | path none cmd/internal/unvendor/golang.org/x/arch 260 | path none cmd/internal/unvendor/golang.org/x/arch/arm 261 | path none cmd/internal/unvendor/golang.org/x/arch/arm/armasm 262 | path none cmd/internal/unvendor/golang.org/x/arch/x86 263 | path none cmd/internal/unvendor/golang.org/x/arch/x86/x86asm 264 | path none cmd/link/internal/mips64 265 | path none cmd/vet/internal 266 | path none cmd/vet/internal/whitelist 267 | path none internal/golang.org 268 | path none internal/golang.org/x 269 | path none internal/golang.org/x/net 270 | path none internal/golang.org/x/net/http2 271 | path none internal/golang.org/x/net/http2/hpack 272 | path none internal/race 273 | path none internal/syscall/windows/sysdll 274 | path none runtime/internal 275 | path none runtime/internal/atomic 276 | path none runtime/internal/sys 277 | path none runtime/msan 278 | none path cmd/internal/rsc.io 279 | none path cmd/internal/rsc.io/arm 280 | none path cmd/internal/rsc.io/arm/armasm 281 | none path cmd/internal/rsc.io/x86 282 | none path cmd/internal/rsc.io/x86/x86asm 283 | none path cmd/vet/whitelist 284 | none path internal/format 285 | ``` 286 | 287 | 对比 "cmd" 目录的变化使用: 288 | 289 | ```shell 290 | $ godocu tree cmd /usr/local/Cellar/go/1.5.2/libexec/src 291 | ``` 292 | 293 | # Diff 294 | 295 | 指令 `diff` 比较输出 source, target 共有包差异, 指令 `first` 仅输出首个差异. 296 | 297 | 如果指定了 `lang`, 对 target 进行 `lang` 过滤, 且以 target 的声明为对比条目. 298 | 299 | 比较 reflect 在当前版本 1.6.2 和老版本的导出声明差异 300 | 301 | ```shell 302 | $ godocu first reflect /usr/local/Cellar/go/1.5.2/libexec/src 303 | ``` 304 | 305 | 输出 306 | 307 | ``` 308 | TEXT: 309 | func DeepEqual(x, y interface{}) bool 310 | DIFF: 311 | func DeepEqual(a1, a2 interface{}) bool 312 | 313 | FROM: package reflect 314 | ``` 315 | 316 | 意思是 317 | 318 | ``` 319 | 内容: 320 | func DeepEqual(x, y interface{}) bool 321 | 不同: 322 | func DeepEqual(a1, a2 interface{}) bool 323 | 324 | 来自: package reflect 325 | ``` 326 | 327 | 比较 os 包在当前版本 1.6.2 和老版本的导出声明差异 328 | 329 | ```shell 330 | $ godocu diff os /usr/local/Cellar/go/1.5.3/libexec/src 331 | ``` 332 | 333 | 输出 334 | 335 | ``` 336 | TEXT: 337 | Type ProcessState struct{pid int; status syscall.WaitStatus; rusage 338 | *syscall.Rusage} 339 | DIFF: 340 | Type ProcessState struct{pid int; status *syscall.Waitmsg} 341 | 342 | TEXT: 343 | func FindProcess(pid int) (*Process, error) 344 | DIFF: 345 | func FindProcess(pid int) (p *Process, err error) 346 | 347 | TEXT: 348 | func Rename(oldpath, newpath string) error 349 | 350 | Rename renames (moves) oldpath to newpath. 351 | If newpath already exists, Rename replaces it. 352 | OS-specific restrictions may apply when oldpath and newpath are in different 353 | directories. 354 | If there is an error, it will be of type *LinkError. 355 | DIFF: 356 | func Rename(oldpath, newpath string) error 357 | 358 | Rename renames (moves) a file. OS-specific restrictions might apply. 359 | If there is an error, it will be of type *LinkError. 360 | 361 | TEXT: 362 | func (*File) Seek(offset int64, whence int) (ret int64, err error) 363 | 364 | Seek sets the offset for the next Read or Write on file to offset, interpreted 365 | according to whence: 0 means relative to the origin of the file, 1 means 366 | relative to the current offset, and 2 means relative to the end. 367 | It returns the new offset and an error, if any. 368 | The behavior of Seek on a file opened with O_APPEND is not specified. 369 | DIFF: 370 | func (*File) Seek(offset int64, whence int) (ret int64, err error) 371 | 372 | Seek sets the offset for the next Read or Write on file to offset, interpreted 373 | according to whence: 0 means relative to the origin of the file, 1 means 374 | relative to the current offset, and 2 means relative to the end. 375 | It returns the new offset and an error, if any. 376 | 377 | FROM: package os 378 | ``` 379 | 380 | 可以看到结构体和注释有些区别. 381 | 382 | Docu 提供了值其实一样, 只是排版格式发生变化的对比, Godocu 只简单比较值 383 | 384 | # List 385 | 386 | list 指令以 JSON 格式输出 Godocu 风格文档清单. 387 | 388 | 如果 `lang` 为空, list 尝试自动提取 `lang`. 389 | 390 | target: 391 | 392 | - 如果 target 为空, 输出到 Stdout. 393 | - 如果 target 为目录, 输出到 target/golist.json 394 | - 如果 target 为 ".json" 文件, 输出到该文件 395 | - 其它报错 396 | 397 | 如果未指定参数 `lang` 则取第一个 Godocu 风格的 lang 值. 398 | 399 | 相关输出结构 400 | 401 | ```go 402 | // List 表示在同一个 repo 下全部包文档信息. 403 | type List struct { 404 | // Repo 是原源代码所在托管 git 仓库地址. 405 | // 如果无法识别值为 "localhost" 406 | Repo string 407 | 408 | // Readme 该 list 或 Repo 的 readme 文件, 自动提取. 409 | Readme string `json:",omitempty"` 410 | 411 | // 文档文件名 412 | Filename string 413 | // Ext 表示除 "go" 格式文档之外的扩展名. 414 | // 例如: "md text" 415 | // 该值由使用者手工设置, Godocu 只是保留它. 416 | Ext string `json:",omitempty"` 417 | 418 | // Subdir 表示文档文件位于 golist.json 所在目录那个子目录. 419 | // 该值由使用者手工设置, Godocu 只是保留它. 420 | Subdir string `json:",omitempty"` 421 | 422 | // Description 该 list 或 Repo 的一句话介绍. 423 | // 该值由使用者手工设置, Godocu 只是保留它. 424 | Description string `json:",omitempty"` 425 | 426 | // Golist 表示额外的 golist 文件, 类似友链接, 可以是本目录的或外部的. 427 | // 该值由使用者手工设置, Godocu 只是保留它. 428 | Golist []string `json:",omitempty"` 429 | 430 | Package []Info // 所有包的信息 431 | } 432 | 433 | // Info 表示单个包文档信息. 434 | type Info struct { 435 | Import string // 导入路径 436 | Synopsis string // 自动提取的一句话包摘要 437 | // Readme 该包下 readme 文件名, 自动提取. 438 | Readme string `json:",omitempty"` 439 | Progress int // 翻译完成度 440 | } 441 | ``` 442 | 443 | 如果 golist.json 已经存在, 那么 Repo, Description, Ext, Subdir 属性被保留. 444 | 否则尝试计算 Repo 地址, 如果是官方包, 那么设定 Repo 为 "github.com/golang/go". 445 | 如果计算 Repo 失败, 那么设定 Repo 为 "localhost". 446 | 447 | *翻译完成度属性 Progress 通过简单比较文档值计算得到,可能与现实不符* 448 | 449 | [Example](#example) 段有详细的例子演示如何配套使用. 450 | 451 | 以 [translations][] 翻译项目为例输出全部包文档清单到 Stdout 的用法有多种: 452 | 453 | ```shell 454 | $ godocu list -goroot=/path/to/github.com/golang-china/golangdoc.translations ... 455 | $ godocu list /path/to/github.com/golang-china/golangdoc.translations/src... 456 | $ cd /path/to/github.com/golang-china/golangdoc.translations 457 | $ godocu list src... 458 | ``` 459 | 460 | - 第一种把翻译项目目录当做 `goroot`. "..." 遍历所有包 461 | - 第二种使用了绝对路径, 注意带上 "/src". 462 | - 第三种使用了当前路径, 是第二种写法的变种. 463 | 464 | 输出: 465 | 466 | ```json 467 | { 468 | "Repo": "github.com/golang/go", 469 | "Filename": "doc_zh_CN.go", 470 | "Package": [ 471 | { 472 | "Import": "", 473 | "Synopsis": "tar包实现了tar格式压缩文件的存取.", 474 | "Progress": 100, 475 | }, 476 | { 477 | "Import": "", 478 | "Synopsis": "zip包提供了zip档案文件的读写服务.", 479 | "Progress": 95, 480 | }, 481 | // ..... 482 | ], 483 | } 484 | ``` 485 | 486 | 487 | 目录关系详见 [Example](#example) 段. 488 | 489 | # Replace 490 | 491 | 指令 `replace` 用 source 的翻译文档替换 target 中未翻译的文档. 492 | 要求 source, target 必须都是翻译文档, 且代码结构一致. 493 | 494 | *使用 `replace` 前, 对 source, target 进行 'merge' 处理可保障代码结构一致.* 495 | 496 | # Example 497 | 498 | 这里以第三方包 go-github 为例: 499 | 500 | - 源包 https://github.com/google/go-github/github 501 | - 翻译 https://github.com/gohub/google 502 | 503 | ```shell 504 | $ go get github.com/google/go-github/github 505 | ``` 506 | 507 | 初次翻译时先生成原源码文档, 假设文档基础路径为 $TARGET, github 上已建立翻译空仓库. 508 | 509 | ```shell 510 | $ cd $TARGET 511 | $ godocu code -lang=zh_cn github.com/google/go-github/github . 512 | ``` 513 | 514 | 此时在 $TARGET 目录下生成: 515 | 516 | ``` 517 | github.com 518 | └── google 519 | └── go-github 520 | └── github 521 | └── doc_zh_CN.go 522 | ``` 523 | 524 | 可见源码包和文档的目录树结构是一致的. Godocu 以此计算导入路径. 525 | 显然 doc_zh_CN.go 中其实是英文文档. 文档翻译请参见 [golang-china][]. 526 | 527 | 接下来是常规的 git 操作: 528 | 529 | ```shell 530 | $ cd $TARGET/github.com/google 531 | $ git init 532 | $ git remote add origin git@github.com:gohub/google.git 533 | ``` 534 | 535 | 如果在使用 Godocu 之前已经做了翻译, 确保目录结构一致即可. 比如: 536 | 537 | ```shell 538 | $ cd $TARGET 539 | $ git clone https://github.com/gohub/google ./github.com/google 540 | ``` 541 | 542 | 现实中的目录树为: 543 | 544 | ``` 545 | github.com 546 | └── google 547 | ├── README.md 548 | ├── go-github 549 | │   └── github 550 | │   └── doc_zh_CN.go 551 | └── golist.json 552 | ``` 553 | 554 | 555 | 之后就可使用 Godocu 提供的指令进行文档操作了. 556 | 557 | 实战, 合并 [Go-zh][] 和 [translations][] 的翻译成果. 558 | 559 | merge 中的 source 可能和翻译的版本不符合 560 | 因为 Go-zh 是基于源码的翻译, merge 的 source 不能使用 ./go-zh/src... 561 | 同理 translations 也应选择配套的版本吧 562 | 最佳情况下应该选择相同的 source 563 | 564 | 下列示意代码假设系统 go 版本和 Go-zh 所用版本一致 565 | 566 | ```shell 567 | $ cd $TARGET # $TARGET 是此实战工作目录, 先克隆两个项目 568 | $ git clone https://github.com/Go-zh/go go-zh 569 | $ git clone https://github.com/golang-china/golangdoc.translations translations 570 | $ # 类似 builtin 那些需要 -u 参数的包要先单独处理, 目标路径会自动建立 571 | $ godocu code ./go-zh/src/builtin go-zh-trans/src -lang=zh_cn -u 572 | $ # 为 Go-zh 生成文档 573 | $ godocu code ./go-zh/src... go-zh-trans/src -lang=zh_cn 574 | $ # 两个项目都合并最新英文文档, merge 保证了结构一致性 575 | $ godocu merge ... go-zh-trans/src -lang=zh_cn 576 | $ godocu merge ... translations/src -lang=zh_cn 577 | $ # 两种方法进行 replace, 结果可能有所不同 578 | $ godocu replace ./go-zh-trans/src... ./translations/src -lang=zh_cn 579 | $ godocu replace ./translations/src... ./go-zh-trans/src -lang=zh_cn 580 | ``` 581 | 582 | *注意: 同时指定 `target`, `lang` 才会生成(覆盖) target, 不然仅输出在 Stdout.* 583 | 584 | 两个项目的目录结构可能和最新官方包不一致, 使用 tree 指令对比, 然后手工处理. 585 | 586 | [docu]: https://godoc.org/github.com/golang-china/godocu/docu 587 | [golang-china]: https://github.com/golang-china/golang-china.github.com 588 | [Go-zh]: https://github.com/Go-zh/go 589 | [translations]: https://github.com/golang-china/golangdoc.translations -------------------------------------------------------------------------------- /docu/abs_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | ) 7 | 8 | func TestAbs(t *testing.T) { 9 | name := `github.com/golang-china/godocu/docu` 10 | abs := Abs(name) 11 | if abs != filepath.Join(GOPATHS[0], "src", name) { 12 | t.Fatal(abs) 13 | } 14 | abs = Abs(".") 15 | if abs != filepath.Join(GOPATHS[0], "src", name) { 16 | t.Fatal(abs) 17 | } 18 | name = "go" 19 | abs = Abs(name) 20 | if abs != filepath.Join(GOROOT, "src", name) { 21 | t.Fatal(abs) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docu/comments.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | ) 8 | 9 | // TranslationProgress 返回 file 的翻译完成度. 10 | // 参数 file 应该是单文件的 Godocu 风格翻译文档. 11 | func TranslationProgress(file *ast.File) int { 12 | var origin, trans int 13 | comments := file.Comments 14 | if _, pos := License(file); pos != -1 { 15 | comments = comments[pos+1:] 16 | } 17 | 18 | count := func(doc *ast.CommentGroup) { 19 | if doc == nil { 20 | return 21 | } 22 | origin++ 23 | pos, src := docPosAndOrigin(comments, doc) 24 | if src != nil && !EqualComment(doc, src) { 25 | trans++ 26 | } 27 | comments = comments[pos+1:] 28 | } 29 | count(file.Doc) 30 | 31 | for _, node := range file.Decls { 32 | switch n := node.(type) { 33 | case *ast.GenDecl: 34 | if n.Tok == token.IMPORT { 35 | continue 36 | } 37 | count(n.Doc) 38 | for _, spec := range n.Specs { 39 | if spec == nil { 40 | continue 41 | } 42 | 43 | switch n.Tok { 44 | case token.VAR, token.CONST: 45 | s, _ := spec.(*ast.ValueSpec) 46 | count(s.Doc) 47 | ClearComment(comments, s.Comment) 48 | case token.TYPE: 49 | s, _ := spec.(*ast.TypeSpec) 50 | count(s.Doc) 51 | ClearComment(comments, s.Comment) 52 | st, ok := s.Type.(*ast.StructType) 53 | if ok && st.Fields != nil { 54 | for _, n := range st.Fields.List { 55 | count(n.Doc) 56 | ClearComment(comments, n.Comment) 57 | } 58 | } 59 | } 60 | } 61 | continue 62 | case *ast.FuncDecl: 63 | count(n.Doc) 64 | } 65 | if len(comments) == 0 { 66 | break 67 | } 68 | } 69 | 70 | if origin == 0 { 71 | return 100 72 | } 73 | return trans * 100 / origin 74 | } 75 | 76 | // License 返回 file 中以 copyright 开头的注释,和该注释的偏移量, 如果有的话. 77 | func License(file *ast.File) (lic string, pos int) { 78 | end := file.Name.Pos() 79 | if file.Doc != nil { 80 | end = file.Doc.Pos() 81 | } 82 | for i, comm := range file.Comments { 83 | if comm == nil { 84 | continue 85 | } 86 | if comm.Pos() >= end { 87 | break 88 | } 89 | lic = comm.Text() 90 | pos = strings.IndexByte(lic, ' ') 91 | if pos != -1 && "copyright" == strings.ToLower(lic[:pos]) { 92 | return lic, i 93 | } 94 | } 95 | return "", -1 96 | } 97 | 98 | // clearFile 清理 file.Comments 中 package 声明之前的注释. 比如 +build 注释. 99 | func clearFile(file *ast.File) { 100 | end := file.Name.Pos() 101 | if file.Doc != nil { 102 | end = file.Doc.Pos() 103 | } 104 | comments := file.Comments 105 | for i := 0; i < len(comments); i++ { 106 | if comments[i].Pos() >= end { 107 | break 108 | } 109 | text := comments[i].Text() 110 | pos := strings.IndexByte(text, ' ') 111 | if pos == -1 { 112 | continue 113 | } 114 | if text[:pos] == "+build" { 115 | comments[i] = nil 116 | } 117 | } 118 | } 119 | 120 | // OriginDoc 返回 file 中的 trans 的原文档. 121 | // 要求 comments 为 GodocuStyle 双语文档, 需要配合 ClearComments, ClearComment. 122 | func OriginDoc(comments []*ast.CommentGroup, trans *ast.CommentGroup) (origin *ast.CommentGroup) { 123 | _, origin = docPosAndOrigin(comments, trans) 124 | return 125 | } 126 | 127 | func docPosAndOrigin(comments []*ast.CommentGroup, trans *ast.CommentGroup) (int, *ast.CommentGroup) { 128 | if trans == nil { 129 | return -1, nil 130 | } 131 | pos := indexOf(comments, trans.Pos()) 132 | if pos > 0 && isOrigin(comments[pos-1], trans.Pos()) { 133 | return pos, comments[pos-1] 134 | } 135 | return pos, nil 136 | } 137 | 138 | // ClearComment 设置 comments 中的 comment 元素为 nil. 139 | // 现实中清除尾注释, 以便 OriginDoc 能正确计算出原文档. 140 | func ClearComment(comments []*ast.CommentGroup, comment *ast.CommentGroup) { 141 | if len(comments) == 0 || comment == nil || len(comment.List) == 0 { 142 | return 143 | } 144 | i := indexOf(comments, comment.Pos()) 145 | if i != -1 { 146 | comments[i] = nil 147 | } 148 | } 149 | 150 | // ClearComments 置 file.Comments 中所有的尾注释元素为 nil. 151 | // 现实中清除尾注释, 以便 OriginDoc 能正确计算出原文档. 152 | func ClearComments(file *ast.File) { 153 | comments := file.Comments 154 | for _, decl := range file.Decls { 155 | switch n := decl.(type) { 156 | case *ast.GenDecl: 157 | for _, spec := range n.Specs { 158 | switch n := spec.(type) { 159 | case *ast.ValueSpec: 160 | ClearComment(comments, n.Comment) 161 | case *ast.ImportSpec: 162 | ClearComment(comments, n.Comment) 163 | case *ast.TypeSpec: 164 | ClearComment(comments, n.Comment) 165 | st, ok := n.Type.(*ast.StructType) 166 | if ok && st.Fields != nil { 167 | for _, n := range st.Fields.List { 168 | ClearComment(comments, n.Comment) 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | // EqualComment 简单比较两个 ast.CommentGroup 值是否一样 178 | func EqualComment(a, b *ast.CommentGroup) bool { 179 | if a == nil || b == nil { 180 | return a == nil && b == nil 181 | } 182 | return len(a.List) == len(b.List) && a.Text() == b.Text() 183 | } 184 | 185 | func indexOf(comments []*ast.CommentGroup, pos token.Pos) int { 186 | for i, cg := range comments { 187 | if cg == nil || cg.Pos() < pos { 188 | continue 189 | } 190 | if cg.Pos() == pos { 191 | return i 192 | } 193 | break 194 | } 195 | return -1 196 | } 197 | 198 | func isOrigin(comment *ast.CommentGroup, trans token.Pos) bool { 199 | if comment == nil { 200 | return false 201 | } 202 | trans -= comment.End() 203 | return trans == 2 || trans == 3 || trans == 4 204 | } 205 | -------------------------------------------------------------------------------- /docu/comments_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | // OK indicates the lack of an error. 10 | 11 | // testOk 表示没有出错 12 | testOk = iota 13 | // testNon non origin 14 | testNon 15 | ) 16 | 17 | type ( 18 | testComment struct { 19 | // origin 20 | 21 | // trans 22 | Comment string 23 | } 24 | ) 25 | 26 | func TestOriginDoc(t *testing.T) { 27 | file := testParseFile(t, "comments_test.go") 28 | _decls, _ := declsOf(TypeNum, file.Decls, 0) 29 | decls := SortDecl(_decls) 30 | 31 | spec, _, _ := decls.SearchSpec("testComment") 32 | if spec == nil { 33 | t.Fatal("Oop! type testComment is nil") 34 | } 35 | ts, _ := spec.(*ast.TypeSpec) 36 | if ts == nil { 37 | t.Fatal("Oop! type testComment is nil") 38 | } 39 | st, _ := ts.Type.(*ast.StructType) 40 | if st == nil { 41 | t.Fatal("Oop! type testComment is nil") 42 | } 43 | field, _ := findField(st.Fields, "Comment") 44 | if field == nil { 45 | t.Fatal("Oop! testComment.Comment is nil") 46 | } 47 | origin := OriginDoc(file.Comments, field.Doc) 48 | if origin.Text() != "origin\n" || field.Doc.Text() != "trans\n" { 49 | t.Fatal(origin.Text(), field.Doc.Text()) 50 | } 51 | 52 | _decls, _ = declsOf(ConstNum, file.Decls, 0) 53 | decls = SortDecl(_decls) 54 | 55 | spec, _, _ = decls.SearchSpec("testOk") 56 | sdoc := SpecDoc(spec) 57 | if sdoc == nil { 58 | t.Fatal("Oop!") 59 | } 60 | if sdoc.Text() != "testOk 表示没有出错\n" { 61 | t.Fatal(sdoc.Text()) 62 | } 63 | origin = OriginDoc(file.Comments, sdoc) 64 | if origin.Text() != "OK indicates the lack of an error.\n" { 65 | t.Fatal(origin.Text()) 66 | } 67 | 68 | spec, _, _ = decls.SearchSpec("testNon") 69 | tdoc := SpecDoc(spec) 70 | if tdoc == nil { 71 | t.Fatal("Oop!") 72 | } 73 | origin = OriginDoc(file.Comments, tdoc) 74 | if origin != nil { 75 | t.Fatal(origin.Text()) 76 | } 77 | want := tdoc.Text() + 78 | GoDocu_Dividing_line + "\n" + 79 | sdoc.Text() 80 | 81 | replaceDoc(file, file, tdoc, sdoc) 82 | got := tdoc.Text() 83 | if got != want { 84 | t.Fatalf("WANT:\n%s\nDIFF:\n%s", want, got) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /docu/diff.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "io" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // FormDiff 对比输出 source, target 的排版或值差异, 返回是否有差异及发生的错误. 11 | func FormDiff(w io.Writer, source, target string) (diff bool, err error) { 12 | if diff = source != target; diff { 13 | err = diffOut(lineString(source) == lineString(target), w, source, target) 14 | } 15 | return 16 | } 17 | 18 | // DiffFormOnly 返回两个字符串是否只是格式不同. 19 | func DiffFormOnly(source, target string) bool { 20 | return source != target && lineString(source) == lineString(target) 21 | } 22 | 23 | // TextDiff 对比输出 source, target 的值差异, 返回是否有差异及发生的错误. 24 | func TextDiff(w io.Writer, source, target string) (diff bool, err error) { 25 | if diff = source != target; diff { 26 | err = diffOut(false, w, source, target) 27 | } 28 | return 29 | } 30 | 31 | func diffOut(form bool, w io.Writer, source, target string) error { 32 | const prefix = " " 33 | if source == "" { 34 | source = "none" 35 | } 36 | if target == "" { 37 | target = "none" 38 | } 39 | if form { 40 | return fprint(w, "FORM:\n", LineWrapper(source, prefix, 80), "DIFF:\n", LineWrapper(target, prefix, 80), nl) 41 | } 42 | return fprint(w, "TEXT:\n", LineWrapper(source, prefix, 80), "DIFF:\n", LineWrapper(target, prefix, 80), nl) 43 | } 44 | 45 | // lineString 对 str 进行单行合并, 剔除空白行 46 | func lineString(str string) string { 47 | s := strings.Split(str, "\n") 48 | str = "" 49 | for i := 0; i < len(s); i++ { 50 | str += " " + strings.TrimSpace(s[i]) 51 | } 52 | return str 53 | } 54 | 55 | // FirstDiff 对比输出两个已排序 ast.File 首个差异, 返回是否有差异及发生的错误. 56 | func FirstDiff(w io.Writer, source, target *ast.File) (diff bool, err error) { 57 | const nl = "\n\n" 58 | diff, err = TextDiff(w, "package "+source.Name.String(), "package "+target.Name.String()) 59 | if diff || err != nil { 60 | return 61 | } 62 | 63 | diff, err = TextDiff(w, source.Doc.Text(), target.Doc.Text()) 64 | if diff || err != nil { 65 | return 66 | } 67 | 68 | diff, err = TextDiff(w, ImportsString(source.Imports), ImportsString(target.Imports)) 69 | if diff || err != nil { 70 | return 71 | } 72 | 73 | return firstDecls(w, source.Decls, target.Decls) 74 | } 75 | 76 | func firstDecls(w io.Writer, source, target []ast.Decl) (diff bool, err error) { 77 | sd, so := declsOf(ConstNum, source, 0) 78 | dd, do := declsOf(ConstNum, target, 0) 79 | diff, err = diffGenDecls(w, "Const ", sd, dd) 80 | if diff || err != nil { 81 | return 82 | } 83 | 84 | sd, so = declsOf(VarNum, source, so) 85 | dd, do = declsOf(VarNum, target, do) 86 | diff, err = diffGenDecls(w, "Var ", sd, dd) 87 | if diff || err != nil { 88 | return 89 | } 90 | 91 | sd, so = declsOf(TypeNum, source, so) 92 | dd, do = declsOf(TypeNum, target, do) 93 | diff, err = diffGenDecls(w, "Type ", sd, dd) 94 | if diff || err != nil { 95 | return 96 | } 97 | 98 | sd, so = declsOf(FuncNum, source, so) 99 | dd, do = declsOf(FuncNum, target, do) 100 | diff, err = diffFuncDecls(w, "Func ", sd, dd) 101 | if diff || err != nil { 102 | return 103 | } 104 | 105 | sd, so = declsOf(MethodNum, source, so) 106 | dd, do = declsOf(MethodNum, target, do) 107 | return diffFuncDecls(w, "Method ", sd, dd) 108 | } 109 | 110 | // Diff 对比输出两个已排序 ast.File 差异, 返回是否有差异及发生的错误. 111 | // 如果包名称不同, 停止继续对比. 112 | func Diff(w io.Writer, source, target *ast.File) (diff bool, err error) { 113 | const nl = "\n\n" 114 | var out bool 115 | diff, err = TextDiff(w, "package "+source.Name.String(), "package "+target.Name.String()) 116 | if diff || err != nil { 117 | return 118 | } 119 | out, err = TextDiff(w, source.Doc.Text(), target.Doc.Text()) 120 | if diff = diff || out; err != nil { 121 | return 122 | } 123 | 124 | out, err = TextDiff(w, ImportsString(source.Imports), ImportsString(target.Imports)) 125 | if diff = diff || out; err != nil { 126 | return 127 | } 128 | 129 | out, err = diffDecls(w, source.Decls, target.Decls) 130 | diff = diff || out 131 | return 132 | } 133 | 134 | // diffDecls 对比输出两个已排序 []ast.Decl 差异, 返回是否有差异及发生的错误. 135 | func diffDecls(w io.Writer, source, target []ast.Decl) (diff bool, err error) { 136 | var out bool 137 | sd, so := declsOf(ConstNum, source, 0) 138 | dd, do := declsOf(ConstNum, target, 0) 139 | out, err = diffGenDecls(w, "Const ", sd, dd) 140 | if diff = diff || out; err != nil { 141 | return 142 | } 143 | 144 | sd, so = declsOf(VarNum, source, so) 145 | dd, do = declsOf(VarNum, target, do) 146 | out, err = diffGenDecls(w, "Var ", sd, dd) 147 | if diff = diff || out; err != nil { 148 | return 149 | } 150 | 151 | sd, so = declsOf(TypeNum, source, so) 152 | dd, do = declsOf(TypeNum, target, do) 153 | out, err = diffGenDecls(w, "Type ", sd, dd) 154 | if diff = diff || out; err != nil { 155 | return 156 | } 157 | 158 | sd, so = declsOf(FuncNum, source, so) 159 | dd, do = declsOf(FuncNum, target, do) 160 | out, err = diffFuncDecls(w, "Func ", sd, dd) 161 | if diff = diff || out; err != nil { 162 | return 163 | } 164 | 165 | sd, so = declsOf(MethodNum, source, so) 166 | dd, do = declsOf(MethodNum, target, do) 167 | out, err = diffFuncDecls(w, "Method ", sd, dd) 168 | diff = diff || out 169 | return 170 | } 171 | 172 | // 需要优化 SortDecl 搜索效率 173 | 174 | func diffGenDecls(w io.Writer, prefix string, source, target []ast.Decl) (diff bool, err error) { 175 | ss := SortDecl(source) 176 | dd := SortDecl(target) 177 | if ss.Len() == 0 && dd.Len() == 0 { 178 | return 179 | } 180 | if ss.Len() == 0 || dd.Len() == 0 { 181 | return TextDiff(w, prefix+strconv.Itoa(ss.Len()), prefix+strconv.Itoa(dd.Len())) 182 | } 183 | var lit string 184 | 185 | for _, node := range ss { 186 | decl := node.(*ast.GenDecl) 187 | for _, spec := range decl.Specs { 188 | lit = SpecIdentLit(spec) 189 | if lit == "_" { 190 | continue 191 | } 192 | targ, _, _ := dd.SearchSpec(lit) 193 | if targ == nil { 194 | diff, err = true, diffOut(false, w, prefix+lit, "") 195 | if err != nil { 196 | return 197 | } 198 | continue 199 | } 200 | // 类型 201 | slit, dlit := SpecTypeLit(spec), SpecTypeLit(targ) 202 | 203 | if slit != dlit { 204 | diff, err = true, diffOut(false, w, prefix+lit+" "+slit, prefix+lit+" "+dlit) 205 | if err != nil { 206 | return 207 | } 208 | continue 209 | } 210 | // 文档 211 | slit, dlit = SpecDoc(spec).Text(), SpecDoc(targ).Text() 212 | 213 | if slit != dlit { 214 | diff, err = true, diffOut(false, w, prefix+lit+" doc:\n\n"+slit, prefix+lit+" doc:\n\n"+dlit) 215 | } 216 | if err != nil { 217 | return 218 | } 219 | } 220 | } 221 | // 第二次只对比没有的 222 | ss, dd = dd, ss 223 | for _, node := range ss { 224 | decl := node.(*ast.GenDecl) 225 | for _, spec := range decl.Specs { 226 | lit = SpecIdentLit(spec) 227 | if lit == "_" { 228 | continue 229 | } 230 | targ, _, _ := dd.SearchSpec(lit) 231 | if targ == nil { 232 | diff, err = true, diffOut(false, w, "", prefix+lit) 233 | if err != nil { 234 | return 235 | } 236 | } 237 | } 238 | } 239 | return 240 | } 241 | 242 | func diffFuncDecls(w io.Writer, prefix string, source, target []ast.Decl) (diff bool, err error) { 243 | ss := SortDecl(source) 244 | dd := SortDecl(target) 245 | if ss.Len() == 0 && dd.Len() == 0 { 246 | return 247 | } 248 | if ss.Len() == 0 || dd.Len() == 0 { 249 | return TextDiff(w, prefix+strconv.Itoa(ss.Len()), prefix+strconv.Itoa(dd.Len())) 250 | } 251 | var lit string 252 | for _, node := range ss { 253 | spec := node.(*ast.FuncDecl) 254 | lit = FuncIdentLit(spec) 255 | targ := dd.Search(lit) 256 | if targ == nil { 257 | diff, err = true, diffOut(false, w, FuncLit(spec), "") 258 | if err != nil { 259 | return 260 | } 261 | continue 262 | } 263 | slit, dlit := FuncLit(spec), FuncLit(targ.(*ast.FuncDecl)) 264 | if slit != dlit { 265 | diff, err = true, diffOut(false, w, slit, dlit) 266 | if err != nil { 267 | return 268 | } 269 | continue 270 | } 271 | sdoc, ddoc := spec.Doc.Text(), targ.(*ast.FuncDecl).Doc.Text() 272 | if sdoc != ddoc { 273 | diff, err = true, diffOut(false, w, slit+"\n\n"+sdoc, dlit+"\n\n"+ddoc) 274 | if err != nil { 275 | return 276 | } 277 | } 278 | } 279 | ss, dd = dd, ss 280 | for _, node := range ss { 281 | spec := node.(*ast.FuncDecl) 282 | lit = FuncIdentLit(spec) 283 | targ := dd.Search(lit) 284 | if targ == nil { 285 | diff, err = true, diffOut(false, w, "", FuncLit(spec)) 286 | if err != nil { 287 | return 288 | } 289 | } 290 | } 291 | return 292 | } 293 | -------------------------------------------------------------------------------- /docu/differ.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode/utf8" 7 | "unsafe" 8 | ) 9 | 10 | func str2bytes(s string) []byte { 11 | x := (*[2]uintptr)(unsafe.Pointer(&s)) 12 | h := [3]uintptr{x[0], x[1], x[1]} 13 | return *(*[]byte)(unsafe.Pointer(&h)) 14 | } 15 | 16 | func bytes2str(b []byte) string { 17 | return *(*string)(unsafe.Pointer(&b)) 18 | } 19 | 20 | // Differ 实现 io.Writer, 用于文本对比, 遇到第一个不同返回带行号的错误. 21 | // 文本尾部的 "\n" 不参与对比. 22 | // 23 | // Differ 的算法比较简单, 最初目的是为了方便测试. 24 | type Differ struct { 25 | name, want string 26 | offset, pos, line int 27 | } 28 | 29 | // NewDiffer 返回一个 Differ 实例. name 用于错误提示, want 用于对比写入数据. 30 | func NewDiffer(name string, want string) *Differ { 31 | return &Differ{name, strings.TrimRight(want, "\n"), 0, 0, 0} 32 | } 33 | 34 | func (d *Differ) error(s string, i int) error { 35 | want := d.want[d.offset:] 36 | pos := strings.IndexByte(want, '\n') 37 | if pos != -1 { 38 | want = want[:pos] 39 | } 40 | pos = strings.LastIndexByte(s[:i], '\n') 41 | if pos != -1 { 42 | s = s[pos+1:] 43 | } else { 44 | pos = strings.LastIndexByte(d.want[:d.pos], '\n') 45 | s = d.want[pos+1:d.pos] + s 46 | } 47 | pos = strings.IndexByte(s, '\n') 48 | if pos != -1 { 49 | s = s[:pos] 50 | } 51 | 52 | if d.offset >= len(d.want) { 53 | return fmt.Errorf("%s:%d:\nWANT: EOF\nDIFF: %q", d.name, d.line+1, 54 | s) 55 | } 56 | return fmt.Errorf("%s:%d:\nWANT: %q\nDIFF: %q", d.name, d.line+1, 57 | want, s) 58 | } 59 | 60 | func (d *Differ) last(s string) (i int, err error) { 61 | for i = 0; i < len(s); i++ { 62 | if s[i] != '\n' { 63 | err = d.error(s, i) 64 | break 65 | } 66 | } 67 | return 68 | } 69 | 70 | // Pos 返回当前的行号和该行第一个字符所在偏移量 71 | func (d *Differ) Pos() (line, offset int) { 72 | return d.line + 1, d.offset 73 | } 74 | 75 | // Write 接收并对比 p, 返回有多少字节的数据一致和不一致的错误信息. 76 | func (d *Differ) Write(p []byte) (n int, err error) { 77 | return d.WriteString(bytes2str(p)) 78 | } 79 | 80 | func (d *Differ) WriteString(s string) (n int, err error) { 81 | for len(s) == 0 { 82 | return 83 | } 84 | 85 | offset := d.offset 86 | max := len(d.want) - d.pos 87 | for i, r := range s { 88 | if i >= max { 89 | return d.last(s[i:]) 90 | } 91 | c, _ := utf8.DecodeRuneInString(d.want[d.pos+i:]) 92 | if r != c { 93 | n, err = i, d.error(s, i) 94 | return 95 | } 96 | if r == '\n' { 97 | d.offset = offset + i + 1 98 | d.line++ 99 | } 100 | } 101 | n = len(s) 102 | d.pos += n 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /docu/docu.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package docu 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "go/ast" 9 | "go/parser" 10 | "go/token" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "strings" 15 | 16 | "golang.org/x/tools/godoc/vfs" 17 | ) 18 | 19 | // Docu 复合 token.FileSet, ast.Package 提供 Go doc 支持. 20 | type Docu struct { 21 | parser.Mode 22 | FileSet *token.FileSet 23 | // astpkg 的 key 就是 import paths. 24 | astpkg map[string]*ast.Package 25 | // Filter 用于生成 astpkg 时过滤文件名和包名. 26 | // 显然文件名包含后缀 ".go", 包名则没有. 27 | Filter func(name string) bool 28 | } 29 | 30 | // New 返回使用 DefaultFilter 进行过滤的 Docu 实例. 31 | func New() *Docu { 32 | return &Docu{parser.ParseComments, token.NewFileSet(), 33 | make(map[string]*ast.Package), DefaultFilter} 34 | } 35 | 36 | // Package 返回 key 对应的 *ast.Package. 37 | // key 为 MergePackageFiles 返回的 paths 元素. 38 | func (du *Docu) Package(key string) *ast.Package { 39 | if du == nil { 40 | return nil 41 | } 42 | pkg, ok := du.astpkg[key] 43 | if !ok || pkg == nil { 44 | return nil 45 | } 46 | return pkg 47 | } 48 | 49 | // NormalLang 返回 key 对应的 *ast.Package 的 lang. 50 | // 当确定该 pk 符合 Docu 命名风格时使用. 51 | func (du *Docu) NormalLang(key string) string { 52 | pkg := du.Package(key) 53 | if pkg == nil || len(pkg.Files) != 1 { 54 | return "" 55 | } 56 | 57 | for abs := range pkg.Files { 58 | base := filepath.Base(abs) 59 | pos := strings.IndexByte(base, '_') + 1 60 | end := strings.IndexByte(base, '.') 61 | if pos == 0 || end <= pos { 62 | break 63 | } 64 | base = base[pos:end] 65 | if IsNormalLang(base) { 66 | return base 67 | } 68 | } 69 | return "" 70 | } 71 | 72 | // GodocuStyle 用来快速识别是否为 Godocu 文件命名风格. 73 | var GodocuStyle = ast.NewIdent("Godocu Style") 74 | 75 | var godocuStyle = []*ast.Ident{GodocuStyle} 76 | 77 | func IsGodocuFile(file *ast.File) bool { 78 | return file != nil && len(file.Unresolved) != 0 && 79 | file.Unresolved[0] == GodocuStyle 80 | } 81 | 82 | // MergePackageFiles 合并 import paths 的包为一个 ast.File 文件, 并用 Index 排序. 83 | // 如果该 file 是 Godocu 文件命名风格, 设置 file.Unresolved[0] = GodocuStyle. 84 | func (du *Docu) MergePackageFiles(key string) (file *ast.File) { 85 | if du == nil || len(du.astpkg) == 0 { 86 | return nil 87 | } 88 | pkg, ok := du.astpkg[key] 89 | if !ok || pkg == nil { 90 | return 91 | } 92 | // 单文件优化 93 | if len(pkg.Files) == 1 { 94 | var name string 95 | for name, file = range pkg.Files { 96 | } 97 | if IsNormalName(filepath.Base(name)) { 98 | file.Unresolved = godocuStyle 99 | } 100 | } else { 101 | // 抛弃无关的 Comments 102 | file = ast.MergePackageFiles(pkg, 103 | ast.FilterFuncDuplicates|ast.FilterUnassociatedComments|ast.FilterImportDuplicates) 104 | // 取出 License 和 import paths 放到 file.Comments 105 | // 通常 License 总第一个 106 | var lic, imp *ast.CommentGroup 107 | for _, f := range pkg.Files { 108 | offset := f.Name.Pos() + token.Pos(len(file.Name.String())) + 1 109 | for _, comm := range f.Comments { 110 | at := comm.Pos() - offset 111 | if at > 0 { 112 | break 113 | } 114 | if lic == nil && at < 0 { 115 | text := comm.Text() 116 | pos := strings.IndexByte(text, ' ') 117 | if pos != -1 && "copyright" == strings.ToLower(text[:pos]) { 118 | lic = comm 119 | continue 120 | } 121 | } 122 | // 简单加入 import paths, 但不检查有效性 123 | if imp == nil && at == 0 && len(comm.List) == 1 && comm.List[0].Slash.IsValid() { 124 | file.Package, file.Name = f.Package, f.Name 125 | imp = comm 126 | break 127 | } 128 | } 129 | if lic != nil && imp != nil { 130 | break 131 | } 132 | } 133 | if lic != nil { 134 | file.Comments = []*ast.CommentGroup{lic} 135 | } 136 | if imp != nil { 137 | file.Comments = append(file.Comments, imp) 138 | } 139 | } 140 | 141 | sort.Sort(SortImports(file.Imports)) 142 | Index(file) 143 | return 144 | } 145 | 146 | // IsMultiplePkgError 返回 err 是否为同一个目录下发生多包冲突. 147 | func IsMultiplePkgError(err error) bool { 148 | return err != nil && strings.HasPrefix(err.Error(), "multiple packages ") 149 | } 150 | 151 | // Parse 解析 path,source 并返回本次解析到的包路径和发生的错误. 152 | // 153 | // 应预先格式化 path,source 组合对应的代码. 154 | // 如果无法确定文件名将产生序号文件名替代. 155 | // 156 | // path: 157 | // import paths 或 Go 文件名 158 | // source: 159 | // nil 160 | // vfs.FileSystem 161 | // []byte,string,io.Reader,*bytes.Buffer 162 | // 163 | // 返回值 importPaths 从 path 计算得到. 164 | func (du *Docu) Parse(path string, source interface{}) (importPaths string, err error) { 165 | var info []os.FileInfo 166 | var fs vfs.FileSystem 167 | var ok bool 168 | 169 | if source == nil { 170 | path = Abs(path) 171 | info, err = du.readFileInfo(path) 172 | if err == errIsFile { 173 | err = nil 174 | path = path[:len(path)-len(info[0].Name())] 175 | } 176 | fs = vfs.OS(path) 177 | } else if fs, ok = source.(vfs.FileSystem); ok { 178 | info, err = fs.ReadDir(path) 179 | } 180 | 181 | if err != nil { 182 | return 183 | } 184 | 185 | if fs != nil { 186 | importPaths, err = du.parseFromVfs(fs, path, info) 187 | return 188 | } 189 | 190 | // 数据方式 191 | abs := Abs(path) 192 | pos := strings.LastIndexAny(abs, `\/`) 193 | if pos != -1 { 194 | path, abs = abs[pos+1:], abs[:pos] 195 | } else { 196 | path = "" 197 | } 198 | importPaths, err = du.parseFile(abs, path, source) 199 | 200 | return 201 | } 202 | 203 | var errIsFile = errors.New("") 204 | 205 | func (du *Docu) readFileInfo(abs string) ([]os.FileInfo, error) { 206 | if fi, e := os.Stat(abs); e != nil { 207 | return nil, e 208 | } else if !fi.IsDir() { 209 | return []os.FileInfo{fi}, errIsFile 210 | } 211 | fd, err := os.Open(abs) 212 | if err != nil { 213 | return nil, err 214 | } 215 | defer fd.Close() 216 | return fd.Readdir(-1) 217 | } 218 | 219 | func (du *Docu) parseFromVfs(fs vfs.FileSystem, dir string, 220 | info []os.FileInfo) (importPaths string, err error) { 221 | 222 | var r vfs.ReadSeekCloser 223 | var paths string 224 | 225 | for _, info := range info { 226 | if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") || 227 | !du.filter(info.Name()) { 228 | 229 | continue 230 | } 231 | if r, err = fs.Open(info.Name()); err == nil { 232 | paths, err = du.parseFile(dir, info.Name(), r) 233 | if err == nil { 234 | err = r.Close() 235 | } else { 236 | r.Close() 237 | } 238 | if err == nil && paths != "" { 239 | if importPaths == "" { 240 | importPaths = paths 241 | } 242 | } 243 | } 244 | if err != nil { 245 | break 246 | } 247 | } 248 | return 249 | } 250 | 251 | func (du *Docu) filter(name string) bool { 252 | return du.Filter == nil || du.Filter(name) 253 | } 254 | 255 | func (du *Docu) parseFile(abs, name string, src interface{}) (string, error) { 256 | var bs []byte 257 | var err error 258 | importPaths := LookImportPath(abs) 259 | if importPaths == "" { 260 | return "", errors.New("LookImportPath fail: " + abs) 261 | } 262 | abs = filepath.Join(abs, name) 263 | bs, err = readSource(abs, src) 264 | if err != nil { 265 | return "", err 266 | } 267 | 268 | if !IsNormalName(name) && !buildForLinux(bs) { 269 | return "", nil 270 | } 271 | 272 | astfile, err := parser.ParseFile(du.FileSet, abs, bs, du.Mode) 273 | if err != nil { 274 | return "", err 275 | } 276 | 277 | name = astfile.Name.String() 278 | // 包名过滤 279 | if !du.filter(name) { 280 | return "", nil 281 | } 282 | 283 | pkg, ok := du.astpkg[importPaths] 284 | if !ok { 285 | pkg = &ast.Package{ 286 | Name: name, 287 | Files: make(map[string]*ast.File), 288 | } 289 | du.astpkg[importPaths] = pkg 290 | } 291 | 292 | if ok { 293 | for oabs, file := range pkg.Files { 294 | if file.Name.String() != name { 295 | return importPaths, fmt.Errorf( 296 | "multiple packages %s (%s) and %s (%s) in %s", 297 | name, filepath.Base(abs), file.Name.String(), filepath.Base(oabs), 298 | filepath.Dir(abs)) 299 | } 300 | } 301 | } 302 | 303 | if _, ok = pkg.Files[abs]; ok { 304 | return "", errors.New("Duplicates: " + abs) 305 | } 306 | pkg.Files[abs] = astfile 307 | 308 | return importPaths, nil 309 | } 310 | -------------------------------------------------------------------------------- /docu/env.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | var ( 10 | GOROOT = strOr(os.Getenv("GOROOT"), runtime.GOROOT()) 11 | GOPATHS = filepath.SplitList(os.Getenv("GOPATH")) 12 | ) 13 | 14 | // via go/build/syslist.go 15 | 16 | const goosList = "android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris windows " 17 | const goarchList = "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc s390 s390x sparc sparc64 " 18 | 19 | // Warehouse 为预定义托管仓库域名. 20 | // 因托管商差异, 依照 Part 计算的仓库地址不一定正确. 21 | var Warehouse = []struct { 22 | Host string // 域名 23 | Part int // 仓库路径占用的段数 24 | }{ 25 | {"github.com", 2}, 26 | {"gopkg.in", 1}, 27 | {"bitbucket.org", 2}, 28 | {"code.google.com", 2}, 29 | {"golang.org", 2}, 30 | {"google.golang.org", 1}, 31 | {"launchpad.net", 2}, 32 | {"git.oschina.net", 2}, 33 | } 34 | 35 | func strOr(a, b string) string { 36 | if a != "" { 37 | return a 38 | } 39 | return b 40 | } 41 | 42 | func exists(path string) bool { 43 | _, err := os.Stat(path) 44 | return err == nil 45 | } 46 | 47 | func existsDir(path string) bool { 48 | info, err := os.Stat(path) 49 | return err == nil && info.IsDir() 50 | } 51 | 52 | func existsFile(path string) bool { 53 | info, err := os.Stat(path) 54 | return err == nil && !info.IsDir() 55 | } 56 | 57 | // Abs 返回 path 的绝对路径. 58 | // 如果 path 疑似绝对路径返回 path. 59 | // 否则在 GOROOT, GOPATHS 中搜索 path 并返回绝对路径. 60 | // 如果未找到返回 path. 61 | func Abs(path string) string { 62 | if path == "" || filepath.IsAbs(path) { 63 | return path 64 | } 65 | if path[0] == '.' { 66 | if abs, err := filepath.Abs(path); err == nil && exists(abs) { 67 | return abs 68 | } 69 | } 70 | 71 | abs, err := filepath.Abs(filepath.Join(GOROOT, "src", path)) 72 | if err == nil && exists(abs) { 73 | return abs 74 | } 75 | 76 | for _, gopath := range GOPATHS { 77 | abs, err := filepath.Abs(filepath.Join(gopath, "src", path)) 78 | if err == nil && exists(abs) { 79 | return abs 80 | } 81 | } 82 | 83 | return path 84 | } 85 | -------------------------------------------------------------------------------- /docu/filter.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | "unicode" 12 | "unicode/utf8" 13 | ) 14 | 15 | var DefaultFilter = PackageFilter 16 | 17 | // PackageFilter 过滤掉 "main","test" 相关包, 过滤掉非 ".go" 文件. 18 | // 使用时应先过滤掉非 ".go" 的文件, 该函数才能正确工作 19 | func PackageFilter(name string) bool { 20 | if name == "" || name[0] == '_' || name[0] == '.' || 21 | name == "main" || name == "test" || name == "main.go" || name == "test.go" { 22 | 23 | return false 24 | } 25 | 26 | if strings.HasSuffix(name, ".go") { 27 | return !strings.HasPrefix(name, "test_") && 28 | !strings.HasPrefix(name, "main_") && 29 | !strings.HasSuffix(name, "_test.go") 30 | } 31 | return strings.IndexByte(name, '.') == -1 32 | } 33 | 34 | // TestFilter 过滤掉非 "test" 相关的包和文件 35 | func TestFilter(name string) bool { 36 | if name == "" || name[0] == '_' || name[0] == '.' { 37 | return false 38 | } 39 | if strings.HasSuffix(name, ".go") { 40 | return name == "test.go" || strings.HasSuffix(name, "_test.go") 41 | } 42 | return name != "main" 43 | } 44 | 45 | // MainFilter 过滤掉非 "main" 相关的包和文件 46 | func MainFilter(name string) bool { 47 | if name == "" || name[0] == '_' || name[0] == '.' { 48 | return false 49 | } 50 | if strings.HasSuffix(name, ".go") { 51 | return name != "test.go" && !strings.HasSuffix(name, "_test.go") && 52 | !strings.HasPrefix(name, "test_") && 53 | !strings.HasPrefix(name, "doc_") 54 | } 55 | 56 | return name == "main" 57 | } 58 | 59 | // GenNameFilter 返回一个过滤函数, 允许所有包名或 go 源文件名等于 filename 通过 60 | func GenNameFilter(filename string) func(string) bool { 61 | return func(name string) bool { 62 | if name == "" || name[0] == '_' || name[0] == '.' { 63 | return false 64 | } 65 | return filename == name || !strings.HasSuffix(name, ".go") 66 | } 67 | } 68 | 69 | // ExportedFileFilter 剔除 non-nil file 中所有非导出声明, 返回该 file 是否具有导出声明. 70 | func ExportedFileFilter(file *ast.File) bool { 71 | return exportedFileFilter(file, nil) 72 | } 73 | 74 | func exportedFileFilter(file *ast.File, by SortDecl) bool { 75 | max := len(file.Decls) 76 | for i := 0; i < max; { 77 | if exportedDeclFilter(file.Decls[i], by) { 78 | i++ 79 | continue 80 | } 81 | copy(file.Decls[i:], file.Decls[i+1:max]) 82 | max-- 83 | } 84 | file.Decls = file.Decls[:max] 85 | return max != 0 86 | } 87 | 88 | // ExportedDeclFilter 剔除 non-nil decl 中所有非导出声明, 返回该 decl 是否具有导出声明. 89 | func ExportedDeclFilter(decl ast.Decl) bool { 90 | return exportedDeclFilter(decl, nil) 91 | } 92 | 93 | func exportedDeclFilter(decl ast.Decl, by SortDecl) bool { 94 | switch decl := decl.(type) { 95 | case *ast.FuncDecl: 96 | if decl.Recv != nil && !exportedRecvFilter(decl.Recv, by) { 97 | return false 98 | } 99 | return decl.Name.IsExported() || nil != by.SearchFunc(FuncIdentLit(decl)) 100 | case *ast.GenDecl: 101 | var ok, wantType, hasType bool 102 | for i := len(decl.Specs); i > 0; { 103 | i-- 104 | if decl.Tok == token.TYPE { 105 | ok = exportedTypeSpecFilter(decl.Specs[i].(*ast.TypeSpec), by) 106 | } else { 107 | ok, hasType = exportedValueSpecFilter(decl.Specs[i].(*ast.ValueSpec), wantType, by) 108 | if ok { 109 | wantType = !hasType 110 | } 111 | } 112 | if !ok { 113 | if i+1 != len(decl.Specs) { 114 | copy(decl.Specs[i:], decl.Specs[i+1:]) 115 | } 116 | decl.Specs = decl.Specs[:len(decl.Specs)-1] 117 | } 118 | } 119 | return len(decl.Specs) != 0 120 | } 121 | return false 122 | } 123 | 124 | func exportedValueSpecFilter(n *ast.ValueSpec, wantType bool, by SortDecl) (bool, bool) { 125 | // 特别情况: 126 | // const ( 127 | // _ Mode = iota 128 | // ModeARM 129 | // ModeThumb 130 | // ) 131 | // 132 | // 和 syscall SOL_SOCKET 133 | // const ( 134 | // _ = iota 135 | // SOL_SOCKET 136 | // ) 137 | hasType := n.Type != nil || len(n.Values) != 0 138 | for i := 0; i < len(n.Names); { 139 | if n.Names[i].IsExported() || wantType && hasType { 140 | i++ 141 | continue 142 | } 143 | if n.Names[i].Name != "_" { 144 | spec, _, _ := by.SearchSpec(n.Names[i].Name) 145 | if spec != nil { 146 | if _, ok := spec.(*ast.ValueSpec); ok { 147 | i++ 148 | continue 149 | } 150 | } 151 | } 152 | 153 | if len(n.Names) == len(n.Values) { 154 | copy(n.Values[i:], n.Values[i+1:]) 155 | n.Values = n.Values[:len(n.Values)-1] 156 | } 157 | copy(n.Names[i:], n.Names[i+1:]) 158 | n.Names = n.Names[:len(n.Names)-1] 159 | } 160 | hasType = n.Type != nil || len(n.Values) != 0 161 | return len(n.Names) != 0, hasType 162 | } 163 | 164 | func exportedTypeSpecFilter(n *ast.TypeSpec, by SortDecl) bool { 165 | var bt *ast.StructType 166 | if !n.Name.IsExported() { 167 | spec, _, _ := by.SearchSpec(n.Name.String()) 168 | if spec == nil { 169 | return false 170 | } 171 | bySpec, ok := spec.(*ast.TypeSpec) 172 | if !ok { 173 | return false 174 | } 175 | bt, _ = bySpec.Type.(*ast.StructType) 176 | } 177 | 178 | st, _ := n.Type.(*ast.StructType) 179 | exportedFieldFilter(st, bt) 180 | return true 181 | } 182 | 183 | // exportedRecvFilter 该方法仅仅适用于检测 ast.FuncDecl.Recv 是否导出 184 | func exportedRecvFilter(fieldList *ast.FieldList, by SortDecl) bool { 185 | for i := 0; i < len(fieldList.List); i++ { 186 | switch n := fieldList.List[i].Type.(type) { 187 | case *ast.Ident: 188 | if spec, _, _ := by.SearchSpec(n.String()); !n.IsExported() && spec == nil { 189 | return false 190 | } 191 | case *ast.StarExpr: 192 | ident, ok := n.X.(*ast.Ident) 193 | if !ok { 194 | return false 195 | } 196 | if spec, _, _ := by.SearchSpec(ident.String()); !ident.IsExported() && spec == nil { 197 | return false 198 | } 199 | } 200 | } 201 | return true 202 | } 203 | 204 | func isExported(name string) bool { 205 | if strings.IndexByte(name, '.') != -1 { 206 | return true 207 | } 208 | ch, _ := utf8.DecodeRuneInString(name) 209 | return unicode.IsUpper(ch) 210 | } 211 | 212 | // 剔除非导出成员 213 | func exportedFieldFilter(n, by *ast.StructType) { 214 | if n == nil || n.Fields == nil { 215 | return 216 | } 217 | list := n.Fields.List 218 | for i := 0; i < len(list); { 219 | names := list[i].Names 220 | // 匿名字段 221 | // type T struct{ 222 | // fmt.Stringer 223 | // } 224 | if len(names) == 0 { 225 | if isExported(types.ExprString(list[i].Type)) { 226 | i++ 227 | } else { 228 | copy(list[i:], list[i+1:]) 229 | list = list[:len(list)-1] 230 | } 231 | continue 232 | } 233 | for i := 0; i < len(names); { 234 | if names[i].IsExported() || by != nil && 235 | hasField(by, names[i].String()) { 236 | i++ 237 | continue 238 | } 239 | copy(names[i:], names[i+1:]) 240 | names = names[:len(names)-1] 241 | } 242 | list[i].Names = names 243 | if len(names) != 0 { 244 | i++ 245 | continue 246 | } 247 | copy(list[i:], list[i+1:]) 248 | list = list[:len(list)-1] 249 | } 250 | n.Fields.List = list 251 | return 252 | } 253 | 254 | func hasField(n *ast.StructType, name string) bool { 255 | if n == nil || n.Fields == nil { 256 | return false 257 | } 258 | _, pos := findField(n.Fields, name) 259 | return pos != -1 260 | } 261 | 262 | func findField(n *ast.FieldList, lit string) (*ast.Field, int) { 263 | for _, field := range n.List { 264 | if field == nil { 265 | continue 266 | } 267 | for i, ident := range field.Names { 268 | if ident.String() == lit { 269 | return field, i 270 | } 271 | } 272 | } 273 | return nil, -1 274 | } 275 | 276 | // WalkPath 可遍历 paths 及其子目录或者独立的文件. 277 | // 若 paths 是包路径或绝对路径, 调用 walkFn 遍历 paths. 278 | func WalkPath(paths string, walkFn filepath.WalkFunc) error { 279 | root := Abs(paths) 280 | info, err := os.Lstat(root) 281 | if err != nil || !info.IsDir() { 282 | return walkFn(root, info, err) 283 | } 284 | 285 | return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 286 | ispkg := IsPkgDir(info) 287 | if err == nil && info.IsDir() { 288 | if info.Name() == "src" { 289 | return nil 290 | } 291 | if !ispkg { 292 | return filepath.SkipDir 293 | } 294 | } 295 | if err != nil || ispkg { 296 | return walkFn(path, info, err) 297 | } 298 | return nil 299 | }) 300 | } 301 | 302 | func walkPath(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { 303 | err := walkFn(path, info, nil) 304 | if err != nil { 305 | if info.IsDir() && err == filepath.SkipDir { 306 | return nil 307 | } 308 | return err 309 | } 310 | 311 | if !info.IsDir() { 312 | return nil 313 | } 314 | 315 | names, err := readDirNames(path) 316 | if err != nil { 317 | return walkFn(path, info, err) 318 | } 319 | 320 | for _, name := range names { 321 | filename := filepath.Join(path, name) 322 | fileInfo, err := os.Lstat(filename) 323 | if err != nil { 324 | if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { 325 | return err 326 | } 327 | } else { 328 | err = walkPath(filename, fileInfo, walkFn) 329 | if err != nil { 330 | if !fileInfo.IsDir() || err != filepath.SkipDir { 331 | return err 332 | } 333 | } 334 | } 335 | } 336 | return nil 337 | } 338 | 339 | func readDirNames(dirname string) ([]string, error) { 340 | f, err := os.Open(dirname) 341 | if err != nil { 342 | return nil, err 343 | } 344 | names, err := f.Readdirnames(-1) 345 | f.Close() 346 | if err != nil { 347 | return nil, err 348 | } 349 | sort.Strings(names) 350 | return names, nil 351 | } 352 | 353 | func IsPkgDir(fi os.FileInfo) bool { 354 | name := fi.Name() 355 | return fi.IsDir() && len(name) > 0 && 356 | name[0] != '_' && name[0] != '.' && 357 | name != "testdata" && name != "vendor" 358 | } 359 | 360 | // CanonicalImportPaths 返回 file 注释中的权威导入路径, 如果有的话. 361 | // 返回值是 import 语句中的字符串部分, 含引号. 362 | func CanonicalImportPaths(file *ast.File) string { 363 | offset := file.Name.Pos() + token.Pos(len(file.Name.String())) + 1 364 | for _, comm := range file.Comments { 365 | if comm == nil { 366 | continue 367 | } 368 | at := comm.Pos() - offset 369 | if at > 0 { 370 | break 371 | } 372 | if at != 0 { 373 | continue 374 | } 375 | if len(comm.List) != 1 || !comm.List[0].Slash.IsValid() || 376 | comm.List[0].Text[1] != '/' { 377 | break 378 | } 379 | text := strings.TrimSpace(comm.Text()) 380 | 381 | if len(text) > 10 && strings.HasPrefix(text, `import "`) && 382 | text[len(text)-1] == '"' { 383 | return text[7:] 384 | } 385 | } 386 | return "" 387 | } 388 | -------------------------------------------------------------------------------- /docu/index.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "sort" 8 | ) 9 | 10 | // 下列常量用于排序 11 | const ( 12 | ImportNum int = iota 13 | ConstNum 14 | VarNum 15 | TypeNum 16 | FuncNum 17 | MethodNum 18 | OtherNum = 1 << 32 19 | ) 20 | 21 | var numNames = []string{ 22 | ImportNum: "import ", 23 | ConstNum: "const ", 24 | VarNum: "var ", 25 | TypeNum: "type ", 26 | FuncNum: "func ", 27 | } 28 | 29 | // NodeNumber 返回值用于节点排序. 随算法更新同类型节点该返回值会变更. 30 | func NodeNumber(node ast.Node) int { 31 | if node == nil { 32 | return OtherNum 33 | } 34 | switch n := node.(type) { 35 | case *ast.GenDecl: 36 | switch n.Tok { 37 | case token.IMPORT: 38 | return ImportNum 39 | case token.CONST: 40 | return ConstNum 41 | case token.VAR: 42 | return VarNum 43 | case token.TYPE: 44 | return TypeNum 45 | } 46 | case *ast.FuncDecl: 47 | if n.Recv == nil { 48 | return FuncNum 49 | } 50 | return MethodNum 51 | } 52 | // BadDecl 或其他 53 | return OtherNum 54 | } 55 | 56 | /* 57 | * SortDecl 实现 sort.Interface. 按 Ident字面值排序. 58 | */ 59 | type SortDecl []ast.Decl 60 | 61 | func (s SortDecl) Len() int { return len(s) } 62 | func (s SortDecl) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 63 | func (s SortDecl) Less(i, j int) bool { 64 | in, jn := NodeNumber(s[i]), NodeNumber(s[j]) 65 | if in != jn { 66 | return in < jn 67 | } 68 | switch in { 69 | default: 70 | si := s[i].(*ast.GenDecl).Specs 71 | sj := s[j].(*ast.GenDecl).Specs 72 | if len(si) == 0 || len(sj) == 0 { 73 | break 74 | } 75 | return SpecIdentLit(si[0]) < SpecIdentLit(sj[0]) 76 | case FuncNum, MethodNum: 77 | return FuncLit(s[i].(*ast.FuncDecl)) < FuncLit(s[j].(*ast.FuncDecl)) 78 | } 79 | return false 80 | } 81 | 82 | // Search 查找 identLit 所在的顶级声明. 83 | func (s SortDecl) Search(identLit string) ast.Decl { 84 | if identLit == "" || identLit == "" { 85 | return nil 86 | } 87 | for _, node := range s { 88 | switch n := node.(type) { 89 | case *ast.GenDecl: 90 | for _, spec := range n.Specs { 91 | if SpecIdentLit(spec) == identLit { 92 | return node 93 | } 94 | } 95 | case *ast.FuncDecl: 96 | lit := FuncIdentLit(n) 97 | if lit == identLit { 98 | return node 99 | } 100 | if lit > identLit { 101 | break 102 | } 103 | default: 104 | break 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | // SearchFunc 查找 funcIdentLit 对应的 ast.FuncDecl. 111 | func (s SortDecl) SearchFunc(funcIdentLit string) *ast.FuncDecl { 112 | if funcIdentLit == "" || funcIdentLit == "" { 113 | return nil 114 | } 115 | for _, node := range s { 116 | switch n := node.(type) { 117 | case *ast.FuncDecl: 118 | lit := FuncIdentLit(n) 119 | if lit == funcIdentLit { 120 | return n 121 | } 122 | if lit > funcIdentLit { 123 | break 124 | } 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | // SearchConstructor 搜索并返回 typeLit 的构建函数声明: 131 | // *typeLit 132 | // *typeLit, error 133 | // typeLit 134 | // typeLit, error 135 | func (s SortDecl) SearchConstructor(typeLit string) *ast.FuncDecl { 136 | if typeLit == "" || typeLit == "" { 137 | return nil 138 | } 139 | for _, node := range s { 140 | if node == nil { 141 | continue 142 | } 143 | switch n := node.(type) { 144 | case *ast.FuncDecl: 145 | if isConstructor(n, typeLit) { 146 | return n 147 | } 148 | } 149 | } 150 | return nil 151 | } 152 | 153 | func isConstructor(n *ast.FuncDecl, typeLit string) bool { 154 | if !n.Name.IsExported() || n.Type.Results == nil { 155 | return false 156 | } 157 | list := n.Type.Results.List 158 | switch len(list) { 159 | case 2: 160 | if types.ExprString(list[1].Type) != "error" { 161 | break 162 | } 163 | fallthrough 164 | case 1: 165 | lit := types.ExprString(list[0].Type) 166 | if lit == typeLit || lit[0] == '*' && lit[1:] == typeLit { 167 | return true 168 | } 169 | } 170 | return false 171 | } 172 | 173 | // SearchSpec 查找 specIdentLit 对应的顶级 ast.Spec 和所在 *ast.GenDecl 以及索引. 174 | func (s SortDecl) SearchSpec(specIdentLit string) (ast.Spec, *ast.GenDecl, int) { 175 | if specIdentLit == "" || specIdentLit == "" { 176 | return nil, nil, -1 177 | } 178 | for _, node := range s { 179 | switch n := node.(type) { 180 | case *ast.GenDecl: 181 | if n.Tok == token.IMPORT { 182 | break 183 | } 184 | for i, spec := range n.Specs { 185 | if SpecIdentLit(spec) == specIdentLit { 186 | return spec, n, i 187 | } 188 | } 189 | } 190 | } 191 | return nil, nil, -1 192 | } 193 | 194 | // Filter 过滤掉 file 中的非导出顶级声明, 如果该声明不在 s 中的话. 195 | func (s SortDecl) Filter(file *ast.File) bool { 196 | return exportedFileFilter(file, s) 197 | } 198 | 199 | // SortImports 实现 sort.Interface. 按照 import path 进行排序. 200 | type SortImports []*ast.ImportSpec 201 | 202 | func (s SortImports) Len() int { return len(s) } 203 | func (s SortImports) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 204 | func (s SortImports) Less(i, j int) bool { 205 | return s[i].Path.Value < s[j].Path.Value 206 | } 207 | 208 | // Index 剔除 file.Decls 中的 import 声明, 并对顶级声明重新排序. 按照: 209 | // 210 | // Consts, Vars, Types, Funcs, Method 211 | func Index(file *ast.File) { 212 | if file != nil { 213 | clearFile(file) 214 | sort.Sort(SortDecl(file.Decls)) 215 | _, offset := declsOf(ImportNum, file.Decls, 0) 216 | for i := 0; i < offset; i++ { 217 | decl := file.Decls[i].(*ast.GenDecl) 218 | _, comment := SpecComment(decl.Specs[0]) 219 | ClearComment(file.Comments, comment) 220 | } 221 | file.Decls = file.Decls[offset:] 222 | } 223 | } 224 | 225 | func declsOf(num int, decls []ast.Decl, offset int) ([]ast.Decl, int) { 226 | first, last := -1, len(decls) 227 | if offset >= 0 && offset < len(decls) { 228 | for i, node := range decls[offset:] { 229 | if first == -1 { 230 | if NodeNumber(node) == num { 231 | first = offset + i 232 | } 233 | } else if NodeNumber(node) > num { 234 | last = offset + i 235 | break 236 | } 237 | } 238 | } 239 | if first == -1 { 240 | return nil, offset 241 | } 242 | return decls[first:last], last 243 | } 244 | 245 | // IndexNormal 对 file 顶级声明进行常规习惯排序, 即: 246 | // 247 | // Consts, Vars, Funcs, Types [Constructor,Method] 248 | // 249 | func IndexNormal(file *ast.File) { 250 | panic("Unimplemented") 251 | sort.Sort(sortNormal(file.Decls)) 252 | } 253 | 254 | var normalOrder = [...]int{ 255 | ImportNum, 256 | ConstNum, 257 | VarNum, 258 | FuncNum, 259 | TypeNum, 260 | MethodNum, 261 | } 262 | 263 | // sortNormal 实现 sort.Interface. 按常规习惯排序. 264 | type sortNormal []ast.Decl 265 | 266 | func (s sortNormal) Len() int { return len(s) } 267 | func (s sortNormal) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 268 | func (s sortNormal) Less(i, j int) bool { 269 | in, jn := NodeNumber(s[i]), NodeNumber(s[j]) 270 | if in == OtherNum || jn == OtherNum || 271 | in != jn && (in <= VarNum || jn <= VarNum) { 272 | return in < jn 273 | } 274 | 275 | if in != jn && (in == FuncNum || jn == FuncNum) { 276 | return normalOrder[in] < normalOrder[jn] 277 | } 278 | // TypeNum, MethodNum 279 | if in == jn { 280 | switch in { 281 | case TypeNum: 282 | si := s[i].(*ast.GenDecl).Specs 283 | sj := s[j].(*ast.GenDecl).Specs 284 | if len(si) == 0 || len(sj) == 0 { 285 | break 286 | } 287 | return SpecIdentLit(si[0]) < SpecIdentLit(sj[0]) 288 | case MethodNum: 289 | return FuncLit(s[i].(*ast.FuncDecl)) < FuncLit(s[j].(*ast.FuncDecl)) 290 | } 291 | } 292 | return false 293 | } 294 | -------------------------------------------------------------------------------- /docu/list.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | // List 表示在同一个 repo 下全部包文档信息. 4 | type List struct { 5 | // Repo 是原源代码所在托管 git 仓库地址. 6 | // 如果无法识别值为 "localhost" 7 | Repo string 8 | 9 | // Readme 该 list 或 Repo 的 readme 文件, 自动提取. 10 | Readme string `json:",omitempty"` 11 | 12 | // 文档文件名 13 | Filename string 14 | // Ext 表示除 "go" 格式文档之外的扩展名. 15 | // 例如: "md text" 16 | // 该值由使用者手工设置, Godocu 只是保留它. 17 | Ext string `json:",omitempty"` 18 | 19 | // Subdir 表示文档文件位于 golist.json 所在目录那个子目录. 20 | // 该值由使用者手工设置, Godocu 只是保留它. 21 | Subdir string `json:",omitempty"` 22 | 23 | // Description 该 list 或 Repo 的一句话介绍. 24 | // 该值由使用者手工设置, Godocu 只是保留它. 25 | Description string `json:",omitempty"` 26 | 27 | // Golist 表示额外的 golist 文件, 类似友链接, 可以是本目录的或外部的. 28 | // 该值由使用者手工设置, Godocu 只是保留它. 29 | Golist []string `json:",omitempty"` 30 | 31 | Package []Info // 所有包的信息 32 | } 33 | 34 | // Info 表示单个包文档信息. 35 | type Info struct { 36 | Import string // 导入路径 37 | Synopsis string // 自动提取的一句话包摘要 38 | // Readme 该包下 readme 文件名, 自动提取. 39 | Readme string `json:",omitempty"` 40 | Progress int // 翻译完成度 41 | } 42 | -------------------------------------------------------------------------------- /docu/lit.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | ) 8 | 9 | // DeclIdentLit 返回返 decl 第一个 ast.Spec 的 Ident 字面描述. 10 | // 如果是 method, 返回风格为 RecvIdentLit.FuncName. 11 | // 如果是 GenDecl 只返回第一个 Spec 的 Ident 字面描述 12 | func DeclIdentLit(decl ast.Decl) (lit string) { 13 | switch n := decl.(type) { 14 | case *ast.GenDecl: 15 | if len(n.Specs) != 0 { 16 | return SpecIdentLit(n.Specs[0]) 17 | } 18 | case *ast.FuncDecl: 19 | return FuncIdentLit(n) 20 | } 21 | return 22 | } 23 | 24 | // SpecIdentLit 返回 spec 首个 Ident 字面描述. 25 | func SpecIdentLit(spec ast.Spec) (lit string) { 26 | switch n := spec.(type) { 27 | case *ast.ValueSpec: 28 | if len(n.Names) != 0 { 29 | lit = n.Names[0].String() 30 | } 31 | case *ast.TypeSpec: 32 | lit = n.Name.String() 33 | } 34 | return 35 | } 36 | 37 | // SpecTypeIdentLit 返回 spec 类型 Ident 字面描述. 38 | func SpecTypeLit(spec ast.Spec) (lit string) { 39 | switch n := spec.(type) { 40 | case *ast.ValueSpec: 41 | lit = types.ExprString(n.Type) 42 | case *ast.TypeSpec: 43 | lit = types.ExprString(n.Type) 44 | } 45 | return 46 | } 47 | 48 | // SpecDoc 返回 spec 的 Doc,Comment 字段 49 | func SpecDoc(spec ast.Spec) *ast.CommentGroup { 50 | if spec == nil { 51 | return nil 52 | } 53 | switch n := spec.(type) { 54 | case *ast.ValueSpec: 55 | return n.Doc 56 | case *ast.TypeSpec: 57 | return n.Doc 58 | } 59 | return nil 60 | } 61 | 62 | // SpecComment 返回 spec 的 Doc,Comment 字段 63 | func SpecComment(spec ast.Spec) (*ast.CommentGroup, *ast.CommentGroup) { 64 | if spec == nil { 65 | return nil, nil 66 | } 67 | switch n := spec.(type) { 68 | case *ast.ValueSpec: 69 | return n.Doc, n.Comment 70 | case *ast.TypeSpec: 71 | return n.Doc, n.Comment 72 | } 73 | return nil, nil 74 | } 75 | 76 | // RecvIdentLit 返回 decl.Recv Ident 字面描述. 不含 decl.Name. 77 | func RecvIdentLit(decl *ast.FuncDecl) (lit string) { 78 | if decl.Recv == nil || len(decl.Recv.List) == 0 { 79 | return 80 | } 81 | switch expr := decl.Recv.List[0].Type.(type) { 82 | case *ast.StarExpr: 83 | if x, ok := expr.X.(fmt.Stringer); ok { 84 | lit = "*" + x.String() 85 | } 86 | case *ast.Ident: 87 | lit = expr.String() 88 | } 89 | return 90 | } 91 | 92 | // FuncIdentLit 返回 FuncDecl 的 Ident 字面描述. 93 | // 如果是 method, 返回风格为 RecvIdentLit.FuncName. 94 | func FuncIdentLit(decl *ast.FuncDecl) (lit string) { 95 | lit = RecvIdentLit(decl) 96 | if lit == "" { 97 | return decl.Name.String() 98 | } 99 | return lit + "." + decl.Name.String() 100 | } 101 | 102 | func FuncParamsLit(decl *ast.FuncDecl) (lit string) { 103 | return "(" + FieldListLit(decl.Type.Params) + ")" 104 | } 105 | 106 | func FuncResultsLit(decl *ast.FuncDecl) (lit string) { 107 | lit = FieldListLit(decl.Type.Results) 108 | if lit != "" && (len(decl.Type.Results.List) > 1 || 109 | len(decl.Type.Results.List[0].Names) != 0) { 110 | lit = "(" + lit + ")" 111 | } 112 | return 113 | } 114 | 115 | // FuncLit 返回 FuncDecl 的字面描述. 不含 decl.Name. 116 | func FuncLit(decl *ast.FuncDecl) (lit string) { 117 | lit = FuncResultsLit(decl) 118 | if lit == "" { 119 | lit = FuncParamsLit(decl) 120 | } else { 121 | lit = FuncParamsLit(decl) + " " + lit 122 | } 123 | if decl.Name != nil { 124 | lit = decl.Name.String() + lit 125 | } 126 | recv := RecvIdentLit(decl) 127 | if recv == "" { 128 | lit = "func " + lit 129 | } else { 130 | lit = "func (" + recv + ") " + lit 131 | } 132 | return 133 | } 134 | 135 | // MethodLit 返回 FuncDecl 的字面描述. 含 decl.Name. 136 | func MethodLit(decl *ast.FuncDecl) (lit string) { 137 | lit = FuncResultsLit(decl) 138 | if lit == "" { 139 | lit = FuncParamsLit(decl) 140 | } else { 141 | lit = FuncParamsLit(decl) + " " + lit 142 | } 143 | if decl.Name != nil { 144 | lit = decl.Name.String() + lit 145 | } 146 | 147 | if decl.Recv != nil && len(decl.Recv.List) != 0 { 148 | lit = "(" + FieldListLit(decl.Recv) + ") " + lit 149 | } 150 | 151 | lit = "func " + lit 152 | return 153 | } 154 | 155 | // FieldListLit 返回 ast.FieldList.List 的字面值. 156 | // 该方法仅适用于: 157 | // ast.FuncDecl.Recv.List 158 | // ast.FuncDecl.Type.Params 159 | // ast.FuncDecl.Type.Results 160 | // 161 | func FieldListLit(list *ast.FieldList) (lit string) { 162 | if list == nil || len(list.List) == 0 { 163 | return 164 | } 165 | for i, field := range list.List { 166 | if i != 0 { 167 | lit += ", " 168 | } 169 | lit += FieldLit(field) 170 | } 171 | return 172 | } 173 | 174 | // FieldLit 返回 ast.Field 的字面值 175 | // 该方法与 FieldListLit 配套使用. 176 | func FieldLit(field *ast.Field) (lit string) { 177 | if field == nil { 178 | return 179 | } 180 | for i, name := range field.Names { 181 | if i == 0 { 182 | lit = name.String() 183 | } else { 184 | lit += ", " + name.String() 185 | } 186 | } 187 | if field.Type != nil { 188 | if lit == "" { 189 | lit = types.ExprString(field.Type) 190 | } else { 191 | lit += " " + types.ExprString(field.Type) 192 | } 193 | } 194 | return 195 | } 196 | 197 | func IdentsLit(idents []*ast.Ident) (lit string) { 198 | if len(idents) == 0 { 199 | return 200 | } 201 | lit = idents[0].String() 202 | for i := 1; i < len(idents); i++ { 203 | lit += ", " + idents[i].String() 204 | } 205 | return 206 | } 207 | 208 | // ImportsString 返回 imports 源码. 209 | func ImportsString(is []*ast.ImportSpec) (s string) { 210 | if len(is) == 0 { 211 | return 212 | } 213 | 214 | if len(is) == 1 { 215 | return "import " + is[0].Path.Value + nl 216 | } 217 | for i, im := range is { 218 | if i == 0 { 219 | s += "import (\n\t" + im.Path.Value + nl 220 | } else { 221 | s += "\t" + im.Path.Value + nl 222 | } 223 | } 224 | s += ")\n" 225 | return 226 | } 227 | -------------------------------------------------------------------------------- /docu/markdown.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | const MarkdownTemplate = `{{define "echo"}} 4 | ` + "```go" + ` 5 | {{.}} 6 | ` + "```" + ` 7 | {{end}}{{/* 8 | 此模板输出 Markdown 格式. 9 | 模板传入 Data 实例作为模板执行数据. 并映射了 docu.FuncsMap. 10 | */}}{{if eq .Key .ImportPath}}{{/* 11 | 模板必须通过 Type 方法(任意位置)设定输出文件扩展名, 否则会抛弃输出. 12 | 在这个例子中只输出标准的 doc 文档, 忽略 main, test 文档. 13 | */}}{{$.Type "md"}}{{$this := .File}}# {{base .ImportPath}} 14 | 15 | {{/* 16 | 函数 progress 返回文档翻译完成度, 值为 0-100. 该值有多种用途. 17 | 如果非 0 显示完成度, 并不输出原语言文档. 如果为 0 等同没有翻译, 不显示. 18 | */}}{{if $trans := progress $this}}Translation Progress: {{$trans}} 19 | 20 | {{end}}{{/* 21 | 函数 canonicalImportPaths 返回文档权威导入路径. 22 | */}}{{if $x := canonicalImportPaths $this}}{{template "echo" $x}}{{end}}{{/* 23 | 主文档以及各种声明 24 | */}}{{if $this.Doc}}{{wrap $this.Doc.Text}}{{end}}{{/* 25 | 26 | 常量 27 | */}}{{range $i, $x := decls $this.Decls .CONST}}{{if eq $i 0}} 28 | ## const 29 | 30 | {{end}}{{$.Text $x}}{{template "echo" $.Code $x}}{{end}}{{/* 31 | */}}{{range $i, $x := decls $this.Decls .VAR}}{{if eq $i 0}} 32 | ## var 33 | 34 | {{end}}{{$.Text $x}}{{template "echo" $.Code $x}}{{end}}{{/* 35 | 36 | 由于未实现常规排序, 只能采取分步剔除的方法 37 | */}}{{$fs:=decls $this.Decls .FUNC}}{{range $i, $x := decls $this.Decls .TYPE}}{{if eq $i 0}} 38 | ## type 39 | 40 | {{end}}{{$lit := identLit $x}} 41 | ### {{$lit}} 42 | 43 | {{$.Text $x}}{{template "echo" $.Code $x}}{{/* 44 | 构造函数 45 | */}}{{$pos := indexConstructor $fs $lit}}{{if ne -1 $pos}}{{$x := index $fs $pos}}{{/* 46 | 47 | */}}{{clear $fs $pos}}{{$.Text $x}}{{template "echo" $.Code $x}}{{end}}{{/* 48 | 成员方法 49 | */}}{{range $m := methods $this.Decls $lit}} 50 | ### {{identLit $m | starLess}} 51 | 52 | {{$.Text $m}}{{template "echo" $.Code $m}}{{end}}{{end}}{{/* 53 | 54 | 函数 55 | */}}{{range $i, $x := trimRight $fs}}{{if eq $i 0}} 56 | ## func 57 | 58 | {{end}}{{if $x}} 59 | ### {{identLit $x}} 60 | 61 | {{$.Text $x}}{{template "echo" $.Code $x}}{{end}}{{end}}{{/* 62 | */}}{{if $x := license $this}} 63 | # License 64 | 65 | {{wrap $x}} 66 | {{end}}{{end}}` 67 | -------------------------------------------------------------------------------- /docu/merge.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | ) 7 | 8 | const GoDocu_Dividing_line = "___GoDocu_Dividing_line___" 9 | 10 | const indent_GoDocu_Dividing_line = " //" + GoDocu_Dividing_line 11 | 12 | // do not change this 13 | var comment_Dividing_line = &ast.Comment{Text: "//___GoDocu_Dividing_line___"} 14 | 15 | // MergeDeclsDoc 添加 source 与 target 中匹配的标识符文档到 target 注释底部 16 | // 细节: 17 | // 只是排版不同不会被合并 18 | // target 应该具有良好的结构, 比如来自源代码 19 | // 用 source 中的尾注释替换 target 中的尾注释 20 | func MergeDeclsDoc(source, target []ast.Decl) { 21 | sd, so := declsOf(ConstNum, source, 0) 22 | dd, do := declsOf(ConstNum, target, 0) 23 | mergeGenDecls(sd, dd) 24 | 25 | sd, so = declsOf(VarNum, source, so) 26 | dd, do = declsOf(VarNum, target, do) 27 | mergeGenDecls(sd, dd) 28 | 29 | sd, so = declsOf(TypeNum, source, so) 30 | dd, do = declsOf(TypeNum, target, do) 31 | mergeGenDecls(sd, dd) 32 | 33 | sd, so = declsOf(FuncNum, source, so) 34 | dd, do = declsOf(FuncNum, target, do) 35 | mergeFuncDecls(sd, dd) 36 | 37 | sd, so = declsOf(MethodNum, source, so) 38 | dd, do = declsOf(MethodNum, target, do) 39 | mergeFuncDecls(sd, dd) 40 | return 41 | } 42 | 43 | // 需要优化 SortDecl 搜索效率 44 | 45 | // mergeGenDecls 负责 ValueSpec, TypeSpec 46 | func mergeGenDecls(source, target []ast.Decl) { 47 | var lit string 48 | var sdoc, tdoc, scomm, tcomm *ast.CommentGroup 49 | if len(source) == 0 || len(target) == 0 { 50 | return 51 | } 52 | trans := SortDecl(source) 53 | 54 | for _, node := range target { 55 | first := true 56 | tdecl := node.(*ast.GenDecl) 57 | for _, tspec := range tdecl.Specs { 58 | lit = SpecIdentLit(tspec) 59 | if lit == "_" { 60 | continue 61 | } 62 | spec, decl, _ := trans.SearchSpec(lit) 63 | if spec == nil { 64 | continue 65 | } 66 | 67 | tdoc, tcomm = SpecComment(tspec) 68 | sdoc, scomm = SpecComment(spec) 69 | 70 | // 解决分组差异 71 | // // target group 72 | // var ( 73 | // // target doc 74 | // target = 1 75 | // ) 76 | // // target doc 77 | // var target = 1 78 | if first { 79 | first = false 80 | if decl.Lparen.IsValid() == tdecl.Lparen.IsValid() { 81 | MergeDoc(decl.Doc, tdecl.Doc) 82 | MergeDoc(sdoc, tdoc) 83 | } else if tdecl.Lparen.IsValid() { 84 | MergeDoc(decl.Doc, tdoc) 85 | } else { 86 | MergeDoc(sdoc, tdecl.Doc) 87 | } 88 | } else { 89 | MergeDoc(sdoc, tdoc) 90 | } 91 | 92 | // 尾注释 93 | if scomm != nil && tcomm != nil { 94 | replaceComment(tcomm, scomm) 95 | } 96 | 97 | if decl.Tok != token.TYPE || tdecl.Tok != token.TYPE { 98 | continue 99 | } 100 | 101 | t, _ := tspec.(*ast.TypeSpec) 102 | s, _ := spec.(*ast.TypeSpec) 103 | if s == nil || t == nil { 104 | continue 105 | } 106 | switch tt := t.Type.(type) { 107 | case *ast.StructType: 108 | ss, _ := s.Type.(*ast.StructType) 109 | if ss != nil && tt != nil { 110 | mergeFieldsDoc(ss.Fields, tt.Fields) 111 | } 112 | case *ast.InterfaceType: 113 | ss, _ := s.Type.(*ast.InterfaceType) 114 | if ss != nil && tt != nil { 115 | mergeFieldsDoc(ss.Methods, tt.Methods) 116 | } 117 | } 118 | } 119 | } 120 | return 121 | } 122 | 123 | // mergeFieldsDoc 保持 target 的结构和尾注释 124 | func mergeFieldsDoc(source, target *ast.FieldList) { 125 | if source == nil || target == nil || 126 | len(source.List) == 0 || len(target.List) == 0 { 127 | return 128 | } 129 | for _, field := range target.List { 130 | if field == nil || field.Doc == nil && field.Comment == nil { 131 | continue 132 | } 133 | for _, ident := range field.Names { 134 | lit := ident.String() 135 | if lit == "_" { 136 | continue 137 | } 138 | f, _ := findField(source, lit) 139 | if f == nil { 140 | continue 141 | } 142 | 143 | MergeDoc(f.Doc, field.Doc) 144 | // 尾注释用替换 145 | replaceComment(field.Comment, f.Comment) 146 | 147 | break 148 | } 149 | } 150 | } 151 | 152 | // MergeDoc 合并 source.List 到 target.list 底部. 153 | // 插入分隔占位字符串 ___GoDocu_Dividing_line___ 154 | func MergeDoc(source, target *ast.CommentGroup) { 155 | if source != nil && target != nil && !EqualComment(source, target) { 156 | target.List = append(target.List, comment_Dividing_line) 157 | target.List = append(target.List, source.List...) 158 | } 159 | } 160 | 161 | func mergeFuncDecls(source, target []ast.Decl) { 162 | ss := SortDecl(source) 163 | dd := SortDecl(target) 164 | if ss.Len() == 0 || dd.Len() == 0 { 165 | return 166 | } 167 | 168 | var lit string 169 | for _, node := range ss { 170 | decl := node.(*ast.FuncDecl) 171 | if decl == nil || decl.Doc == nil { 172 | continue 173 | } 174 | 175 | lit = FuncIdentLit(decl) 176 | tdecl := dd.SearchFunc(lit) 177 | if tdecl != nil { 178 | MergeDoc(decl.Doc, tdecl.Doc) 179 | } 180 | } 181 | return 182 | } 183 | -------------------------------------------------------------------------------- /docu/merge_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "testing" 10 | ) 11 | 12 | type ( 13 | testTypeOrigin struct { 14 | // a 15 | a []string // a 16 | // b 17 | b string // b 18 | // cd 19 | c, d string // cd 20 | e string 21 | testing.T 22 | } 23 | 24 | testTypeTrans struct { 25 | // trans a 26 | a []string // trans a 27 | // trans b 28 | b string // b // trans b 29 | // trans cd 30 | c string // trans cd 31 | d, e string 32 | } 33 | ) 34 | 35 | const testmergeTypeWant = `type ( 36 | testTypeOrigin struct { 37 | // a 38 | 39 | // trans a 40 | a []string // a 41 | 42 | // b 43 | 44 | // trans b 45 | b string // b // trans b 46 | 47 | // cd 48 | 49 | // trans cd 50 | c, d string // cd 51 | e string 52 | testing.T 53 | } 54 | 55 | testTypeTrans struct { 56 | // trans a 57 | a []string // trans a 58 | 59 | // trans b 60 | b string // b // trans b 61 | 62 | // trans cd 63 | c string // trans cd 64 | d, e string 65 | } 66 | )` 67 | 68 | func specStructType(spec ast.Spec) *ast.StructType { 69 | if spec == nil { 70 | return nil 71 | } 72 | ts, _ := spec.(*ast.TypeSpec) 73 | if ts == nil { 74 | return nil 75 | } 76 | st, _ := ts.Type.(*ast.StructType) 77 | return st 78 | } 79 | 80 | func TestMergeFieldsDoc(t *testing.T) { 81 | file := testParseFile(t, "merge_test.go") 82 | _decls, _ := declsOf(TypeNum, file.Decls, 0) 83 | decls := SortDecl(_decls) 84 | spec, decl, _ := decls.SearchSpec("testTypeOrigin") 85 | st := specStructType(spec) 86 | if st == nil { 87 | t.Fatal("Oop!") 88 | } 89 | 90 | tests := []struct { 91 | name string 92 | doc string 93 | comment string 94 | pos int 95 | }{ 96 | {"a", "a\n", "a\n", 0}, 97 | {"b", "b\n", "b\n", 0}, 98 | {"c", "cd\n", "cd\n", 0}, 99 | {"d", "cd\n", "cd\n", 1}, 100 | {"e", "", "", 0}, 101 | } 102 | for _, tt := range tests { 103 | field, i := findField(st.Fields, tt.name) 104 | if field == nil || i != tt.pos { 105 | t.Fatalf("findField(%q) %d", tt.name, i) 106 | } 107 | text := field.Doc.Text() 108 | 109 | if text != tt.doc { 110 | t.Fatalf("findField(%q) %d doc %q", tt.name, i, text) 111 | } 112 | 113 | text = field.Comment.Text() 114 | if text != tt.comment { 115 | t.Fatalf("findField(%q) %d comment %q", tt.name, i, text) 116 | } 117 | } 118 | 119 | tspec, _, _ := decls.SearchSpec("testTypeTrans") 120 | tt := specStructType(tspec) 121 | if tt == nil { 122 | t.Fatal("Oop!") 123 | } 124 | // 注意 target, source 次序 125 | mergeFieldsDoc(tt.Fields, st.Fields) 126 | tests = []struct { 127 | name string 128 | doc string 129 | comment string 130 | pos int 131 | }{ 132 | {"a", "a\n___GoDocu_Dividing_line___\ntrans a\n", "a\n", 0}, 133 | {"b", "b\n___GoDocu_Dividing_line___\ntrans b\n", "b // trans b\n", 0}, 134 | {"c", "cd\n___GoDocu_Dividing_line___\ntrans cd\n", "cd\n", 0}, 135 | {"d", "cd\n___GoDocu_Dividing_line___\ntrans cd\n", "cd\n", 1}, 136 | {"e", "", "", 0}, 137 | } 138 | 139 | for _, tt := range tests { 140 | field, i := findField(st.Fields, tt.name) 141 | if field == nil || i != tt.pos { 142 | t.Fatalf("findField(%q) %d", tt.name, i) 143 | } 144 | text := field.Doc.Text() 145 | 146 | if text != tt.doc { 147 | t.Fatalf("findField(%q) %d doc %q", tt.name, i, text) 148 | } 149 | 150 | text = field.Comment.Text() 151 | if text != tt.comment { 152 | t.Fatalf("findField(%q) %d comment %q", tt.name, i, text) 153 | } 154 | } 155 | 156 | var buf bytes.Buffer 157 | err := FprintGenDecl(&buf, decl, nil) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | text := buf.String() 162 | _, err = NewDiffer("Differ", testmergeTypeWant).WriteString(text) 163 | if err != nil { 164 | t.Fatalf("%s\n%s\n%s", err, "TEXT:", text) 165 | } 166 | } 167 | 168 | func TestMergeDeclsDoc(t *testing.T) { 169 | var buf bytes.Buffer 170 | origin := testParseFile(t, "testdata/origin.go") 171 | ExportedFileFilter(origin) 172 | Fprint(&buf, origin) 173 | text := buf.String() 174 | _, err := testWantDiffer(t, "testdata/code_origin.text").WriteString(text) 175 | if err != nil { 176 | t.Fatalf("%s\n%s\n%s", err, "TEXT:", text) 177 | } 178 | 179 | trans := testParseFile(t, "testdata/trans.go") 180 | MergeDoc(trans.Doc, origin.Doc) 181 | MergeDeclsDoc(trans.Decls, origin.Decls) 182 | buf.Reset() 183 | Fprint(&buf, origin) 184 | text = buf.String() 185 | _, err = testWantDiffer(t, "testdata/merge_origin_trans.text").WriteString(text) 186 | if err != nil { 187 | t.Fatalf("%s\n%s\n%s", err, "TEXT:", text) 188 | } 189 | } 190 | 191 | func testParseFile(t *testing.T, name string) *ast.File { 192 | file, err := parser.ParseFile(token.NewFileSet(), name, nil, parser.ParseComments) 193 | if err != nil { 194 | t.Fatal("Oop! ParseFile", name, err.Error()) 195 | } 196 | Index(file) 197 | return file 198 | } 199 | 200 | func testWantDiffer(t *testing.T, name string) *Differ { 201 | bs, err := ioutil.ReadFile(name) 202 | if err != nil { 203 | t.Fatal(name, err.Error()) 204 | } 205 | return NewDiffer(name, string(bs)) 206 | } 207 | -------------------------------------------------------------------------------- /docu/performance_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import "testing" 4 | 5 | func BenchmarkDocu_Parse(b *testing.B) { 6 | for i := 0; i < b.N; i++ { 7 | du := New() 8 | du.Parse("go/types", nil) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docu/printer.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "go/doc" 7 | "go/printer" 8 | "go/token" 9 | "go/types" 10 | "io" 11 | "strings" 12 | "text/tabwriter" 13 | "unicode" 14 | 15 | "golang.org/x/text/width" 16 | ) 17 | 18 | const nl = "\n" 19 | 20 | // Resultsify 返回 " (results)" , 如果 results 含有空格的话. 21 | func Resultsify(results string) string { 22 | if results == "" { 23 | return results 24 | } 25 | if strings.IndexByte(results, ' ') == -1 { 26 | return " " + results 27 | } 28 | return " (" + results + ")" 29 | } 30 | 31 | var prefix = []string{"// ", "\t// ", "\t\t// "} 32 | var indents = []string{"", "\xff\t\xff", "\xff\t\t\xff"} 33 | var rawindents = []string{"", "\t", "\t\t"} 34 | 35 | // Format 调用 LineWrapper 换行格式化注释 doc 输出到 output. 36 | // indent 是 "\t" 缩进个数, 值范围为 0,1,2. 37 | // 如果 doc 是合并文档, 包含 GoDocu_Dividing_line, 表示输出双语文档. 38 | // 如果 doc 非双语文档且 comments 非 nil, 则在 comments 中查找并输出 OriginDoc. 39 | func Format(output io.Writer, indent int, 40 | doc *ast.CommentGroup, comments []*ast.CommentGroup) (err error) { 41 | if doc == nil { 42 | return 43 | } 44 | source, text := SplitComments(doc.Text()) 45 | if source == "" && comments != nil { 46 | source = OriginDoc(comments, doc).Text() 47 | } 48 | if source == "" && text == "" { 49 | return 50 | } 51 | if source != "" { 52 | source = WrapComments(source, prefix[indent], 77-indent*4) 53 | } 54 | if text != "" { 55 | text = WrapComments(text, prefix[indent], 77-indent*4) 56 | } 57 | tw, istw := output.(*tabwriter.Writer) 58 | // 防止 Wrap 后结果一样 59 | if source != "" && source != text { 60 | if istw { 61 | err = fprint(output, tabEscapes, source, tabEscapes, nl) 62 | } else { 63 | err = fprint(output, source, nl) 64 | } 65 | } 66 | if text != "" && err == nil { 67 | if istw { 68 | err = fprint(output, tabEscapes, text, tabEscapes) 69 | } else { 70 | err = fprint(output, text) 71 | } 72 | } 73 | if err == nil && tw != nil { 74 | err = tw.Flush() 75 | } 76 | return 77 | } 78 | func isNotSpace(r rune) bool { 79 | return !unicode.IsSpace(r) 80 | } 81 | 82 | // WrapComments 对 text 连续行进行折叠后调用 LineWrapper. 83 | func WrapComments(text, prefix string, limit int) string { 84 | var buf bytes.Buffer 85 | if text == "" { 86 | return "" 87 | } 88 | offset := wrappedBefor(text, limit) 89 | if offset == len(text) { 90 | limit = 1 << 32 91 | } else { 92 | doc.ToText(&buf, text, "", "\t", 1<<32) 93 | text = buf.String() 94 | } 95 | return LineWrapper(text, prefix, limit) 96 | } 97 | 98 | // wrappedBefor 返回 text 首个行长度大于 limit 的行首偏移量. 99 | // 如果满足 limit, 返回 len(text) 100 | // tab 按四个长度计算, 多字节按两个长度计算. 101 | func wrappedBefor(text string, limit int) int { 102 | offset, w := 0, 0 103 | 104 | for i, r := range text { 105 | if w == 0 { 106 | offset = i 107 | } 108 | switch r { 109 | case '\n': 110 | w = 0 111 | w += 4 112 | case '\t': 113 | w = w/4*4 + 4 114 | default: 115 | if r > unicode.MaxLatin1 { 116 | w += 2 117 | } else { 118 | w++ 119 | } 120 | } 121 | if w > limit { 122 | return offset 123 | } 124 | } 125 | return len(text) 126 | } 127 | 128 | // SplitComments 以 GoDocu_Dividing_line 分割 text 为两部分. 129 | // 如果没有分割线返回 "",text 130 | func SplitComments(text string) (string, string) { 131 | n := strings.Index(text, GoDocu_Dividing_line) 132 | if n == -1 { 133 | return "", text 134 | } 135 | return text[:n], text[n+len(GoDocu_Dividing_line)+1:] 136 | } 137 | 138 | // UrlPos 识别 text 第一个网址出现的位置. 139 | func UrlPos(text string) (pos int) { 140 | pos = strings.Index(text, "://") 141 | if pos <= 0 { 142 | return -1 143 | } 144 | pos-- 145 | for pos != 0 { 146 | if (text[pos] >= 'a' && text[pos] <= 'z') || text[pos] == '-' { 147 | pos-- 148 | } else { 149 | pos++ 150 | break 151 | } 152 | } 153 | return 154 | } 155 | 156 | func urlEnd(r rune) bool { 157 | return r == 0 || runeWidth(r) == 2 || unicode.IsSpace(r) 158 | } 159 | 160 | func firstWidth(text string) (w int) { 161 | for _, r := range text { 162 | if r == '\n' { 163 | return 164 | } 165 | if r > unicode.MaxLatin1 { 166 | w += 2 167 | } else { 168 | w++ 169 | } 170 | } 171 | return 172 | } 173 | 174 | // KeepPunct 当这些标点符号位于折行处时, 前一个词会被折到下一行. 175 | // 已过时, 未来会删除 176 | var KeepPunct = `,.:;?,.:;?。` 177 | 178 | // WrapPunct 当这些标点符号位于行尾时, 会被折到下一行. 179 | // 已过时,未来会删除 180 | var WrapPunct = "`!*@" + `"'[(“([` 181 | 182 | func runeWidth(r rune) int { 183 | if r == 0 { 184 | return 0 185 | } 186 | if r > unicode.MaxLatin1 { 187 | switch width.LookupRune(r).Kind() { 188 | case width.EastAsianAmbiguous, width.EastAsianWide, width.EastAsianFullwidth: 189 | return 2 190 | } 191 | } 192 | return 1 193 | } 194 | 195 | // 返回 text 第一行内不超过 limit 的位置 196 | func limitPos(text string, limit int) (int, rune) { 197 | const keepPunct = "`!*@" + `,.:;?,.:;?。"'[(“([` 198 | 199 | var s, ps, ww int 200 | var pr, sr rune 201 | w := strings.IndexByte(text, '\n') 202 | if text[0] == '\t' || strings.HasPrefix(text, " ") { 203 | if w == -1 { 204 | return len(text), 0 205 | } 206 | return w, '\n' 207 | } 208 | 209 | w, ww = 0, 1 210 | for i, r := range text { 211 | if r == '\n' { 212 | return i, r 213 | } 214 | ps, pr = s, sr 215 | if r == '\t' { 216 | w = w/4*4 + 4 217 | s, sr = i, r 218 | } else { 219 | if r == ' ' || r == ' ' { 220 | s, sr = i, r 221 | } 222 | } 223 | width := runeWidth(r) 224 | if width == 2 || ww != width { 225 | s, sr = i, r 226 | } 227 | ww = width 228 | // 连续长字符串 229 | if s == 0 || w+ww <= limit { 230 | w += ww 231 | continue 232 | } 233 | if s != i || ps == 0 || strings.IndexRune(keepPunct, r) == -1 { 234 | return s, sr 235 | } 236 | // 回退单词, 保持标点符号 237 | return ps, pr 238 | } 239 | return len(text), 0 240 | } 241 | 242 | // LineWrapper 把 text 非缩进行超过显示长度 limit 的行插入换行符 "\n". 243 | // 细节: 244 | // text 行间 tab 按 4 字节宽度计算. 245 | // prefix 为每行前缀字符串. 246 | // limit 的长度不包括 prefix 的长度. 247 | // 返回 wrap 的尾部带换行 248 | func LineWrapper(text string, prefix string, limit int) (wrap string) { 249 | for len(text) != 0 { 250 | last := "" 251 | pos, r := limitPos(text, limit) 252 | 253 | if pos <= limit && pos != len(text) && !urlEnd(r) { 254 | // 保证网址完整性 255 | i := UrlPos(text[:pos]) 256 | if i != -1 { 257 | pos = strings.IndexFunc(text[i:], unicode.IsSpace) 258 | if pos == -1 { 259 | pos = len(text) - i 260 | } 261 | if i != 0 { 262 | last = text[:i] 263 | wrap += strings.TrimRightFunc(prefix+last, unicode.IsSpace) + nl 264 | } 265 | last, text = text[i:pos], text[i+pos:] 266 | wrap += strings.TrimRightFunc(prefix+last, unicode.IsSpace) + nl 267 | 268 | if len(text) != pos && text[pos] == '\n' { 269 | text = text[pos+1:] 270 | } else { 271 | text = strings.TrimLeftFunc(text[pos:], unicode.IsSpace) 272 | } 273 | continue 274 | } 275 | } 276 | 277 | last = text[:pos] 278 | 279 | wrap += strings.TrimRightFunc(prefix+last, unicode.IsSpace) + nl 280 | 281 | if len(text) != pos && r == '\n' { 282 | text = text[pos+1:] 283 | } else { 284 | text = strings.TrimLeftFunc(text[pos:], unicode.IsSpace) 285 | } 286 | } 287 | // 剔除前部和尾部多余的空白行 288 | prefix = strings.TrimRightFunc(prefix, unicode.IsSpace) + nl 289 | for strings.HasPrefix(wrap, prefix) { 290 | wrap = wrap[len(prefix):] 291 | } 292 | for strings.HasSuffix(wrap, prefix) { 293 | wrap = wrap[:len(wrap)-len(prefix)] 294 | } 295 | if wrap != "" && wrap[len(wrap)-1] != '\n' { 296 | wrap += nl 297 | } 298 | return 299 | } 300 | 301 | func fprint(output io.Writer, ss ...string) (err error) { 302 | if output != nil { 303 | for i := 0; err == nil && i < len(ss); i++ { 304 | if ss[i] != "" { 305 | _, err = io.WriteString(output, ss[i]) 306 | } 307 | } 308 | } 309 | return 310 | } 311 | 312 | var config = printer.Config{Mode: printer.TabIndent, Tabwidth: 4} 313 | var emptyfset = token.NewFileSet() 314 | var tabEscape = []byte{tabwriter.Escape} 315 | var tabEscapes = string(tabEscape) 316 | 317 | func fprintExpr(w io.Writer, expr ast.Expr, before ...string) (err error) { 318 | if err = fprint(w, before...); err == nil { 319 | err = config.Fprint(w, emptyfset, expr) 320 | } 321 | return 322 | } 323 | 324 | // Fprint 以 go source 风格向 output 输出已排序的 ast.File. 325 | func Fprint(output io.Writer, file *ast.File) (err error) { 326 | var text string 327 | var comments []*ast.CommentGroup 328 | 329 | if IsGodocuFile(file) { 330 | comments = file.Comments 331 | } 332 | 333 | if text, _ = License(file); text != "" { 334 | err = fprint(output, LineWrapper(text, "// ", 77), nl) 335 | } 336 | if err == nil { 337 | err = fprint(output, "// +build ingore\n\n") 338 | } 339 | 340 | if err == nil { 341 | err = Format(output, 0, file.Doc, comments) 342 | } 343 | if err != nil { 344 | return 345 | } 346 | 347 | text = file.Name.String() 348 | 349 | if imp := CanonicalImportPaths(file); imp != "" { 350 | text += ` // ` + imp 351 | } 352 | 353 | err = fprint(output, "package ", text, nl+nl) 354 | 355 | if err == nil && len(file.Imports) != 0 { 356 | err = fprint(output, ImportsString(file.Imports), nl) 357 | } 358 | 359 | if err != nil { 360 | return 361 | } 362 | for _, node := range file.Decls { 363 | switch n := node.(type) { 364 | case *ast.GenDecl: 365 | switch n.Tok { 366 | default: 367 | continue 368 | case token.CONST, token.VAR: 369 | err = FprintGenDecl(output, n, comments) 370 | case token.TYPE: 371 | err = FprintGenDecl(output, n, comments) 372 | } 373 | case *ast.FuncDecl: 374 | err = FprintFuncDecl(output, n, comments) 375 | } 376 | if err == nil { 377 | err = fprint(output, nl) 378 | } 379 | if err != nil { 380 | break 381 | } 382 | } 383 | return 384 | } 385 | 386 | // FprintFuncDecl 向 w 输出顶级函数声明 fn. comments 用于输出双语文档. 387 | func FprintFuncDecl(w io.Writer, fn *ast.FuncDecl, comments []*ast.CommentGroup) (err error) { 388 | err = Format(w, 0, fn.Doc, comments) 389 | if err == nil { 390 | err = fprint(w, MethodLit(fn), nl) 391 | } 392 | return 393 | } 394 | 395 | // FprintGenDecl 向 w 输出顶级声明 decl. comments 用于输出双语文档. 396 | func FprintGenDecl(w io.Writer, decl *ast.GenDecl, comments []*ast.CommentGroup) (err error) { 397 | if decl == nil || len(decl.Specs) == 0 || decl.Tok == token.IMPORT { 398 | return 399 | } 400 | if err = Format(w, 0, decl.Doc, comments); err != nil { 401 | return 402 | } 403 | 404 | switch decl.Tok { 405 | case token.CONST: 406 | err = fprint(w, "const ") 407 | case token.VAR: 408 | err = fprint(w, "var ") 409 | case token.TYPE: 410 | err = fprint(w, "type ") 411 | } 412 | 413 | if err != nil { 414 | return 415 | } 416 | 417 | indent := 0 418 | tw := NewWriter(w) 419 | if decl.Lparen.IsValid() { 420 | indent = 1 421 | err = fprint(tw, "(\n") 422 | } 423 | 424 | out := false 425 | for _, spec := range decl.Specs { 426 | if spec == nil { 427 | continue 428 | } 429 | switch decl.Tok { 430 | case token.CONST, token.VAR: 431 | vs := spec.(*ast.ValueSpec) 432 | if out && vs.Doc != nil { 433 | if err = fprint(tw, nl); err != nil { 434 | break 435 | } 436 | } 437 | err = FprintValueSpec(tw, indent, vs, comments) 438 | case token.TYPE: 439 | vs := spec.(*ast.TypeSpec) 440 | if out { 441 | if err = fprint(tw, "\f"); err != nil { 442 | break 443 | } 444 | } 445 | err = FprintTypeSpec(tw, indent, vs, comments) 446 | } 447 | 448 | if err != nil { 449 | break 450 | } 451 | out = true 452 | } 453 | 454 | if out && err == nil && decl.Lparen.IsValid() { 455 | err = fprint(tw, ")\n") 456 | } 457 | 458 | if err == nil { 459 | err = tw.Flush() 460 | } 461 | return 462 | } 463 | 464 | // NewWriter 返回适用 Docu 的 tabwriter.Writer 实例. 465 | func NewWriter(w io.Writer) *tabwriter.Writer { 466 | tw, ok := w.(*tabwriter.Writer) 467 | if !ok { 468 | tw = tabwriter.NewWriter(w, 0, 4, 1, ' ', 469 | tabwriter.StripEscape|tabwriter.DiscardEmptyColumns) 470 | } 471 | return tw 472 | } 473 | 474 | // FprintValueSpec 向 w 输出 vs. indent 是 tab 缩进个数, comments 用于输出双语文档. 475 | func FprintValueSpec(w *tabwriter.Writer, indent int, 476 | vs *ast.ValueSpec, comments []*ast.CommentGroup) (err error) { 477 | 478 | if err = Format(w, indent, vs.Doc, comments); err == nil { 479 | err = fprint(w, indents[indent], IdentsLit(vs.Names)) 480 | } 481 | if err != nil { 482 | return 483 | } 484 | 485 | if vs.Type != nil { 486 | err = fprintExpr(w, vs.Type, "\v") 487 | } else if len(vs.Values) != 0 || vs.Comment != nil { 488 | err = fprint(w, "\v") 489 | } else { 490 | err = fprint(w, nl) 491 | return 492 | } 493 | 494 | for i, expr := range vs.Values { 495 | if i == 0 { 496 | err = fprintExpr(w, expr, "\v= ") 497 | } else { 498 | err = fprintExpr(w, expr, ", ") 499 | } 500 | if err != nil { 501 | return 502 | } 503 | } 504 | if err != nil { 505 | return 506 | } 507 | if vs.Comment != nil { 508 | if len(vs.Values) == 0 { 509 | if err = fprint(w, "\v"); err != nil { 510 | return 511 | } 512 | } 513 | err = fprint(w, "\v", tabEscapes, 514 | trimNL(comments, vs.Comment), tabEscapes, nl) 515 | } else { 516 | err = fprint(w, nl) 517 | } 518 | return 519 | } 520 | 521 | // FprintTypeSpec 向 w 输出 ts. indent 是 tab 缩进个数, comments 用于输出双语文档. 522 | func FprintTypeSpec(w *tabwriter.Writer, indent int, 523 | ts *ast.TypeSpec, comments []*ast.CommentGroup) (err error) { 524 | 525 | if err = Format(w, indent, ts.Doc, comments); err == nil { 526 | err = fprint(w, indents[indent], ts.Name.String()) 527 | } 528 | if err != nil { 529 | return 530 | } 531 | 532 | if st, ok := ts.Type.(*ast.StructType); ok { 533 | fprint(w, " struct {\f") 534 | if err = FprintFieldList(w, indent+1, st.Fields, comments); err == nil { 535 | err = fprint(w, indents[indent], "}\f") 536 | } 537 | return 538 | } 539 | 540 | if st, ok := ts.Type.(*ast.InterfaceType); ok { 541 | fprint(w, " interface {\f") 542 | if err = FprintMethods(w, indent+1, st.Methods, comments); err == nil { 543 | err = fprint(w, indents[indent], "}\f") 544 | } 545 | return 546 | } 547 | 548 | err = fprintExpr(w, ts.Type, "\v") 549 | if err != nil { 550 | return 551 | } 552 | if ts.Comment != nil { 553 | err = fprint(w, "\v", tabEscapes, 554 | trimNL(comments, ts.Comment), tabEscapes, nl) 555 | } else { 556 | err = fprint(w, nl) 557 | } 558 | return 559 | } 560 | 561 | // FprintFieldList 向 w 输出 fields. indent 是 tab 缩进个数, comments 用于输出双语文档. 562 | func FprintFieldList(w *tabwriter.Writer, indent int, fields *ast.FieldList, comments []*ast.CommentGroup) (err error) { 563 | for i, field := range fields.List { 564 | if field.Doc != nil { 565 | // 注释前加换行 566 | if i != 0 { 567 | fprint(w, nl) 568 | } 569 | if err = Format(w, indent, field.Doc, comments); err != nil { 570 | break 571 | } 572 | } 573 | if len(field.Names) == 0 { 574 | err = fprintExpr(w, field.Type, indents[indent]) 575 | } else { 576 | err = fprintExpr(w, field.Type, indents[indent], IdentsLit(field.Names), "\v") 577 | } 578 | if err == nil && field.Tag != nil { 579 | err = fprint(w, " ", field.Tag.Value) 580 | } 581 | 582 | if err == nil && field.Comment != nil { 583 | err = fprint(w, "\v", tabEscapes, 584 | trimNL(comments, field.Comment), tabEscapes, nl) 585 | } else if err == nil { 586 | err = fprint(w, nl) 587 | } 588 | if err != nil { 589 | break 590 | } 591 | } 592 | if err == nil { 593 | err = w.Flush() 594 | } 595 | return 596 | } 597 | 598 | // FprintMethods 向 w 输出接口的 methods. indent 是 tab 缩进个数, comments 用于输出双语文档. 599 | func FprintMethods(w *tabwriter.Writer, indent int, methods *ast.FieldList, comments []*ast.CommentGroup) (err error) { 600 | for i, field := range methods.List { 601 | if field.Doc != nil { 602 | // 注释前加换行 603 | if i != 0 { 604 | fprint(w, nl) 605 | } 606 | if err = Format(w, indent, field.Doc, comments); err != nil { 607 | return 608 | } 609 | } 610 | if ftyp, isFtyp := field.Type.(*ast.FuncType); isFtyp { 611 | // method 612 | lit := FieldListLit(ftyp.Results) 613 | 614 | if lit != "" && (len(ftyp.Results.List) > 1 || 615 | len(ftyp.Results.List[0].Names) != 0) { 616 | lit = " (" + lit + ")" 617 | } 618 | fprint(w, indents[indent], field.Names[0].String(), 619 | "("+FieldListLit(ftyp.Params)+")", lit) 620 | } else { 621 | // embedded interface 622 | fprint(w, indents[indent], types.ExprString(field.Type)) 623 | } 624 | 625 | if field.Comment != nil { 626 | fprint(w, "\t", tabEscapes, 627 | trimNL(comments, field.Comment), tabEscapes, nl) 628 | } else { 629 | fprint(w, nl) 630 | } 631 | } 632 | err = w.Flush() 633 | return 634 | } 635 | 636 | func trimNL(comments []*ast.CommentGroup, comment *ast.CommentGroup) string { 637 | ClearComment(comments, comment) 638 | isblock := len(comment.List) != 0 && comment.List[0].Text[1] == '*' 639 | 640 | s := comment.Text() 641 | l := len(s) 642 | if l > 0 && s[l-1] == '\n' { 643 | s = s[:l-1] 644 | } 645 | if len(s) != 0 { 646 | if isblock { 647 | s = "/*" + s + "*/" 648 | } else { 649 | s = "// " + s 650 | } 651 | } 652 | return s 653 | } 654 | -------------------------------------------------------------------------------- /docu/printer_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import "testing" 4 | 5 | func TestWrapComments(t *testing.T) { 6 | tests := []struct { 7 | text string 8 | want string 9 | }{ 10 | {"", ""}, {" ", ""}, {"\n", ""}, {"\t\n", ""}, 11 | { 12 | "the mode, using the given Block. The length of iv must be the same as the Block's", 13 | "// the mode, using the given Block. The length of iv must be the same as the\n// Block's\n", 14 | }, 15 | { 16 | "InsertAfter inserts a new element e with value v immediately after mark and returns e.", 17 | "// InsertAfter inserts a new element e with value v immediately after mark and\n// returns e.\n", 18 | }, 19 | { 20 | "Pop removes the minimum element (according to Less) from the heap and returns it.", 21 | "// Pop removes the minimum element (according to Less) from the heap and returns\n// it.\n", 22 | }, 23 | { 24 | "注意接口的Push和Pop方法是供heap包调用的,请使用heap.Push和heap.Pop来向一个堆添加或者删除元素。", 25 | "// 注意接口的Push和Pop方法是供heap包调用的,请使用heap.Push和heap.Pop来向一个堆\n// 添加或者删除元素。\n", 26 | }, 27 | { 28 | "tab\n\tcode\n\n注意接口的Push和Pop方法是供heap包调用的,请使用heap.Push和heap.Pop来向一个堆添加或者删除元素。", 29 | "// tab\n//\n// \tcode\n//\n// 注意接口的Push和Pop方法是供heap包调用的,请使用heap.Push和heap.Pop来向一个堆\n// 添加或者删除元素。\n", 30 | }, 31 | { 32 | "将一个Stream与一个io.Writer接口关联起来,Write方法会调用XORKeyStream方法来处理提供的所有切片。如果Write方法返回的n小于提供的切片的长度,则表示StreamWriter不同步,必须丢弃。StreamWriter没有内建的缓存,不需要调用Close方法去清空缓存。", 33 | "// 将一个Stream与一个io.Writer接口关联起来,Write方法会调用XORKeyStream方法来处\n// 理提供的所有切片。如果Write方法返回的n小于提供的切片的长度,则表示\n" + 34 | "// StreamWriter不同步,必须丢弃。StreamWriter没有内建的缓存,不需要调用Close方法\n// 去清空缓存。\n", 35 | }, 36 | { 37 | "SignPSS采用RSASSA-PSS方案计算签名。注意hashed必须是使用提供给本函数的hash参数对(要签名的)原始数据进行hash的结果。opts参数可以为nil,此时会使用默认参数。\n", 38 | "// SignPSS采用RSASSA-PSS方案计算签名。注意hashed必须是使用提供给本函数的hash参数\n// 对(要签名的)原始数据进行hash的结果。opts参数可以为nil,此时会使用默认参数。\n", 39 | }, 40 | { 41 | "一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七八。九十", 42 | "// 一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一二三四五六七\n// 八。九十\n", 43 | }, 44 | { 45 | "http://123.456.com/3243214324234354354353245345435435435435435435435435342543543 yes", 46 | "// http://123.456.com/3243214324234354354353245345435435435435435435435435342543543\n// yes\n", 47 | }, 48 | { 49 | "see http://123.456.com/3243214324234354354353245345435435435435435435435435342543543", 50 | "// see\n// http://123.456.com/3243214324234354354353245345435435435435435435435435342543543\n", 51 | }, 52 | { 53 | "see http://123.456.com/3243214324234354354353245345435435435435435435435433333 reame.me", 54 | "// see\n// http://123.456.com/3243214324234354354353245345435435435435435435435433333\n// reame.me\n", 55 | }, 56 | { 57 | "see\n http://123.456.com/3243214324234354354353245345435435435435435435435433333 reame.me", 58 | "// see\n//\n// \thttp://123.456.com/3243214324234354354353245345435435435435435435435433333 reame.me\n", 59 | }, 60 | } 61 | for _, tt := range tests { 62 | if got := WrapComments(tt.text, "// ", 77); got != tt.want { 63 | t.Errorf("WrapComments(%q) =\n%q\nwant\n%q", tt.text, got, tt.want) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docu/replace.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | ) 8 | 9 | // Replace 用 source 翻译文档替换 target 中相匹配的 Ident 的翻译文档. 10 | // 细节: 11 | // target, source 必须是双语翻译文档 12 | // 忽略 ImportSpec 13 | // 替换后 target 中的文档 Text() 改变, Pos(), End() 不变. 14 | func Replace(target, source *ast.File) { 15 | 16 | if !IsGodocuFile(source) || !IsGodocuFile(target) { 17 | return 18 | } 19 | replaceDoc(target, source, target.Doc, source.Doc) 20 | 21 | sd, so := declsOf(ConstNum, source.Decls, 0) 22 | dd, do := declsOf(ConstNum, target.Decls, 0) 23 | replaceGenDecls(target, source, dd, sd) 24 | 25 | sd, so = declsOf(VarNum, source.Decls, so) 26 | dd, do = declsOf(VarNum, target.Decls, do) 27 | replaceGenDecls(target, source, dd, sd) 28 | 29 | sd, so = declsOf(TypeNum, source.Decls, so) 30 | dd, do = declsOf(TypeNum, target.Decls, do) 31 | replaceGenDecls(target, source, dd, sd) 32 | 33 | sd, so = declsOf(FuncNum, source.Decls, so) 34 | dd, do = declsOf(FuncNum, target.Decls, do) 35 | replaceFuncDecls(target, source, dd, sd) 36 | 37 | sd, so = declsOf(MethodNum, source.Decls, so) 38 | dd, do = declsOf(MethodNum, target.Decls, do) 39 | replaceFuncDecls(target, source, dd, sd) 40 | return 41 | } 42 | 43 | // 需要优化 SortDecl 搜索效率 44 | 45 | // replaceGenDecls 负责 ValueSpec, TypeSpec 46 | func replaceGenDecls(dst, src *ast.File, target, source []ast.Decl) { 47 | var lit string 48 | var sdoc, tdoc, scomm, tcomm *ast.CommentGroup 49 | if len(source) == 0 || len(target) == 0 { 50 | return 51 | } 52 | dd := SortDecl(target) 53 | 54 | for _, node := range source { 55 | decl := node.(*ast.GenDecl) 56 | first := true 57 | for _, spec := range decl.Specs { 58 | lit = SpecIdentLit(spec) 59 | if lit == "_" { 60 | continue 61 | } 62 | // 必须清理尾注释 63 | sdoc, scomm = SpecComment(spec) 64 | ClearComment(src.Comments, scomm) 65 | 66 | tspec, tdecl, _ := dd.SearchSpec(lit) 67 | if tspec == nil { 68 | continue 69 | } 70 | 71 | tdoc, tcomm = SpecComment(tspec) 72 | 73 | // 尾注释 74 | ClearComment(dst.Comments, tcomm) 75 | replaceComment(tcomm, scomm) 76 | 77 | // 独立注释 78 | if sdoc != decl.Doc && tdoc != tdecl.Doc { 79 | replaceDoc(dst, src, tdoc, sdoc) 80 | } 81 | 82 | // 分组或者非分组注释 83 | if first && decl.Lparen.IsValid() == tdecl.Lparen.IsValid() { 84 | replaceDoc(dst, src, tdecl.Doc, decl.Doc) 85 | } 86 | first = false 87 | 88 | if decl.Tok != token.TYPE { 89 | continue 90 | } 91 | // StructType 92 | stype, _ := spec.(*ast.TypeSpec) 93 | ttype, _ := tspec.(*ast.TypeSpec) 94 | 95 | st, _ := stype.Type.(*ast.StructType) 96 | tt, _ := ttype.Type.(*ast.StructType) 97 | if st == nil || tt == nil { 98 | continue 99 | } 100 | replaceFieldsDoc(dst, src, tt.Fields, st.Fields) 101 | } 102 | } 103 | return 104 | } 105 | 106 | func replaceFieldsDoc(dst, src *ast.File, target, source *ast.FieldList) { 107 | if source == nil || target == nil || 108 | len(source.List) == 0 || len(target.List) == 0 { 109 | return 110 | } 111 | for _, field := range target.List { 112 | if field == nil || field.Doc == nil && field.Comment == nil { 113 | continue 114 | } 115 | for _, ident := range field.Names { 116 | lit := ident.String() 117 | if lit == "_" { 118 | continue 119 | } 120 | // 必须清理尾注释 121 | ClearComment(dst.Comments, field.Comment) 122 | f, _ := findField(source, lit) 123 | if f == nil { 124 | continue 125 | } 126 | // 尾注释 127 | ClearComment(src.Comments, f.Comment) 128 | replaceComment(field.Comment, f.Comment) 129 | replaceDoc(dst, src, field.Doc, f.Doc) 130 | break 131 | } 132 | } 133 | } 134 | 135 | // replaceComment 替换尾注释 136 | func replaceComment(target, source *ast.CommentGroup) { 137 | if source == nil || target == nil || EqualComment(source, target) || 138 | strings.Index(target.Text(), " // ") != -1 || 139 | strings.Index(source.Text(), " // ") == -1 { 140 | return 141 | } 142 | ReplaceDoc(target, source) 143 | } 144 | 145 | func replaceDoc(dst, src *ast.File, target, source *ast.CommentGroup) { 146 | // source 必须是翻译, 且 target 无 origin 才能替换 147 | // 其实是合并 148 | if target == nil || OriginDoc(src.Comments, source) == nil { 149 | return 150 | } 151 | 152 | if OriginDoc(dst.Comments, target) == nil { 153 | MergeDoc(source, target) 154 | } 155 | } 156 | 157 | // ReplaceDoc 替换 target.List 为 source.list. 158 | // 保持 target.Pos(), target.End() 不变 159 | func ReplaceDoc(target, source *ast.CommentGroup) { 160 | pos, end := target.Pos(), target.End() 161 | if !pos.IsValid() { 162 | pos = 1 << 30 163 | } 164 | if !end.IsValid() { 165 | end = 1 << 30 166 | } 167 | 168 | if len(target.List) != 0 { 169 | target.List = target.List[:0] 170 | } 171 | target.List = append(target.List, source.List...) 172 | cg := target.List[0] 173 | cg.Slash = pos 174 | if len(target.List) > 1 { 175 | cg = target.List[len(target.List)-1] 176 | cg.Slash = token.Pos(int(end) - len(cg.Text)) 177 | } 178 | } 179 | 180 | func replaceFuncDecls(dst, src *ast.File, target, source []ast.Decl) { 181 | ss := SortDecl(source) 182 | dd := SortDecl(target) 183 | if ss.Len() == 0 || dd.Len() == 0 { 184 | return 185 | } 186 | 187 | for _, node := range ss { 188 | decl := node.(*ast.FuncDecl) 189 | if decl.Doc == nil { 190 | continue 191 | } 192 | 193 | tdecl := dd.SearchFunc(FuncIdentLit(decl)) 194 | if tdecl == nil { 195 | continue 196 | } 197 | 198 | if tdecl.Doc == nil { 199 | if OriginDoc(src.Comments, decl.Doc) != nil { 200 | tdecl.Doc = decl.Doc 201 | } 202 | continue 203 | } 204 | replaceDoc(dst, src, tdecl.Doc, decl.Doc) 205 | } 206 | return 207 | } 208 | -------------------------------------------------------------------------------- /docu/replace_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestReplace(t *testing.T) { 9 | var buf bytes.Buffer 10 | source := testParseFile(t, "testdata/replace_source.text") 11 | target := testParseFile(t, "testdata/merge_origin_trans.text") 12 | source.Unresolved = godocuStyle 13 | target.Unresolved = godocuStyle 14 | 15 | Replace(target, source) 16 | 17 | err := Fprint(&buf, target) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | text := buf.String() 23 | _, err = testWantDiffer(t, "testdata/replace_source_merge_origin_trans.text").WriteString(text) 24 | if err != nil { 25 | t.Fatalf("%s\n%s\n%s", err, "TEXT:", text) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docu/target.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "go/ast" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // SrcElem 本地文件系统中的 "/src/". 15 | const SrcElem = string(os.PathSeparator) + "src" + string(os.PathSeparator) 16 | 17 | // IsNormalName 返回 name 是否符合 Docu 命名风格. 18 | // name 必须具有扩展名. 19 | func IsNormalName(name string) bool { 20 | pos := strings.LastIndexByte(name, '.') 21 | if pos == -1 || pos+1 == len(name) { 22 | return false 23 | } 24 | name = name[:pos] 25 | ss := strings.SplitN(name, "_", 2) 26 | return len(ss) == 2 && 27 | (ss[0] == "doc" || ss[0] == "main" || ss[0] == "test") && 28 | IsNormalLang(ss[1]) 29 | } 30 | 31 | var declPackage = []byte("\npackage ") 32 | var plusBuild = []byte("+build") 33 | var linux = []byte("linux") 34 | var slashslash = []byte("//") 35 | 36 | func buildForLinux(code []byte) bool { 37 | pos := bytes.Index(code, declPackage) 38 | if pos == -1 { 39 | return bytes.HasPrefix(code, declPackage[1:]) 40 | } 41 | code = code[:pos+1] 42 | 43 | for len(code) != 0 { 44 | pos = bytes.IndexByte(code, '\n') 45 | line := code[:pos] 46 | code = code[pos+1:] 47 | if !bytes.HasPrefix(line, slashslash) { 48 | continue 49 | } 50 | line = bytes.TrimSpace(line[2:]) 51 | if !bytes.HasPrefix(line, plusBuild) { 52 | continue 53 | } 54 | line = line[len(plusBuild):] 55 | pos = bytes.Index(line, linux) 56 | return pos > 0 && line[pos-1] == ' ' && 57 | (pos+len(linux) == len(line) || line[pos+len(linux)] == ' ') 58 | } 59 | return true 60 | } 61 | 62 | func IsNormalLang(lang string) bool { 63 | ss := strings.SplitN(lang, "_", 2) 64 | if lang = ss[0]; lang == "" { 65 | return false 66 | } 67 | for i := 0; i < len(lang); i++ { 68 | if lang[i] < 'a' || lang[i] > 'z' { 69 | return false 70 | } 71 | } 72 | if len(ss) == 1 { 73 | return true 74 | } 75 | if lang = ss[1]; lang == "" { 76 | return false 77 | } 78 | for i := 0; i < len(lang); i++ { 79 | if lang[i] < 'A' || lang[i] > 'Z' { 80 | return false 81 | } 82 | } 83 | return true 84 | } 85 | 86 | // LangOf 返回 Godocu 命名风格的 name 中的 lang 部分. 87 | // 如果 name 不符合 Godocu 命名风格返回空. 88 | func LangOf(name string) string { 89 | pos := strings.IndexByte(name, '.') 90 | if pos == -1 { 91 | return "" 92 | } 93 | name = name[:pos] 94 | 95 | pos = strings.IndexByte(name, '_') 96 | if pos == -1 { 97 | return "" 98 | } 99 | switch name[:pos] { 100 | default: 101 | return "" 102 | case "doc", "main", "test": 103 | } 104 | if !IsNormalLang(name[pos+1:]) { 105 | return "" 106 | } 107 | return name[pos+1:] 108 | } 109 | 110 | // LangNormal 对 lang 进行检查并格式化. 111 | // 如果 lang 不符合要求, 返回空字符串. 112 | func LangNormal(lang string) string { 113 | ss := strings.Split(strings.ToLower(lang), "_") 114 | lang = ss[0] 115 | if len(ss) > 2 || lang == "" { 116 | return "" 117 | } 118 | if len(ss) == 2 { 119 | if ss[1] == "" { 120 | return "" 121 | } 122 | lang += "_" + strings.ToUpper(ss[1]) 123 | } 124 | for i := 0; i < len(lang); i++ { 125 | if lang[i] == '_' || 126 | lang[i] >= 'a' && lang[i] <= 'z' || 127 | lang[i] >= 'A' && lang[i] <= 'Z' { 128 | continue 129 | } 130 | return "" 131 | } 132 | return lang 133 | } 134 | 135 | // NormalPkgFileName 返回符合 Docu 命名风格的 pkg 所使用的文件名. 136 | // 如果 pkg 不符合 Docu 命名风格返回空字符串. 137 | func NormalPkgFileName(pkg *ast.Package) string { 138 | if pkg == nil || len(pkg.Files) != 1 { 139 | return "" 140 | } 141 | 142 | for abs := range pkg.Files { 143 | abs = filepath.Base(abs) 144 | if !strings.HasSuffix(abs, ".go") || !IsNormalName(abs) { 145 | break 146 | } 147 | return abs 148 | } 149 | 150 | return "" 151 | } 152 | 153 | // LookReadme 在 path 下搜寻 readme 文件名. 未找到返回空串. 154 | func LookReadme(path string) string { 155 | names, err := readDirNames(path) 156 | if err != nil { 157 | return "" 158 | } 159 | 160 | for _, name := range names { 161 | if !strings.HasSuffix(name, ".go") && 162 | strings.HasPrefix(strings.ToLower(name), "readme") { 163 | info, err := os.Lstat(filepath.Join(path, name)) 164 | if err == nil && !info.IsDir() { 165 | return name 166 | } 167 | } 168 | } 169 | return "" 170 | } 171 | 172 | // LookImportPath 返回绝对目录路径 abs 中的 import paths 值. 未找到返回 "" 173 | func LookImportPath(abs string) string { 174 | if abs == "" { 175 | return "" 176 | } 177 | if abs[len(abs)-1] == os.PathSeparator { 178 | abs = abs[:len(abs)-1] 179 | } 180 | 181 | if strings.HasSuffix(abs, SrcElem[:4]) { 182 | return "" 183 | } 184 | 185 | pos := strings.Index(abs, SrcElem) 186 | if pos != -1 { 187 | return filepath.ToSlash(abs[pos+5:]) 188 | } 189 | for _, wh := range Warehouse { 190 | pos = strings.Index(abs, wh.Host) 191 | if pos == -1 { 192 | continue 193 | } 194 | if abs[pos-1] == os.PathSeparator && abs[pos+len(wh.Host)] == os.PathSeparator { 195 | return filepath.ToSlash(abs[pos:]) 196 | } 197 | } 198 | 199 | return "" 200 | } 201 | 202 | // OSArchTest 提取并返回 go 文件名 name 中可识别的 goos, goarch, test 部分. 203 | // name_$(GOOS).* 204 | // name_$(GOARCH).* 205 | // name_$(GOOS)_$(GOARCH).* 206 | // name_$(GOOS)_test.* 207 | // name_$(GOARCH)_test.* 208 | // name_$(GOOS)_$(GOARCH)_test.* 209 | func OSArchTest(name string) (goos, goarch string, test bool) { 210 | if !strings.HasSuffix(name, ".go") { 211 | return 212 | } 213 | name = name[:len(name)-3] 214 | if len(name) == 0 { 215 | return 216 | } 217 | l := strings.Split(name, "_")[1:] 218 | n := len(l) 219 | if n == 0 { 220 | return 221 | } 222 | if l[n-1] == "test" { 223 | test = true 224 | n-- 225 | l = l[:n] 226 | } 227 | if n == 0 { 228 | return 229 | } 230 | if n >= 2 { 231 | l, n = l[n-2:], 2 232 | } 233 | 234 | s := l[n-1] 235 | if contains(goosList, s) { 236 | goos = s 237 | } else if contains(goarchList, s) { 238 | goarch = s 239 | } 240 | if n == 1 || goos == "" && goarch == "" { 241 | return 242 | } 243 | s = l[0] 244 | if goos == "" && contains(goosList, s) { 245 | goos, s = s, "" 246 | } 247 | if goarch == "" && contains(goarchList, s) { 248 | goarch = s 249 | } 250 | return 251 | } 252 | 253 | func contains(s, sep string) bool { 254 | pos := strings.Index(s, sep) 255 | if pos == -1 { 256 | return false 257 | } 258 | if len(sep) == len(s) { 259 | return true 260 | } 261 | if pos == 0 { 262 | return s[pos+len(sep)] == ' ' 263 | } 264 | if pos+len(sep) == len(s) { 265 | return s[pos-1] == ' ' 266 | } 267 | return s[pos-1] == ' ' && s[pos+len(sep)] == ' ' 268 | } 269 | 270 | func readSource(filename string, src interface{}) ([]byte, error) { 271 | if src != nil { 272 | switch s := src.(type) { 273 | case string: 274 | return []byte(s), nil 275 | case []byte: 276 | return s, nil 277 | case *bytes.Buffer: 278 | // is io.Reader, but src is already available in []byte form 279 | if s != nil { 280 | return s.Bytes(), nil 281 | } 282 | case io.Reader: 283 | var buf bytes.Buffer 284 | if _, err := io.Copy(&buf, s); err != nil { 285 | return nil, err 286 | } 287 | return buf.Bytes(), nil 288 | } 289 | return nil, errors.New("invalid source") 290 | } 291 | return ioutil.ReadFile(filename) 292 | } 293 | -------------------------------------------------------------------------------- /docu/target_test.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import "testing" 4 | 5 | func TestIsNormalName(t *testing.T) { 6 | tests := []struct { 7 | want bool 8 | name string 9 | }{ 10 | {false, "doc"}, 11 | {false, "doc.go"}, 12 | {false, "doc.md"}, 13 | {true, "main_zh_CN.md"}, 14 | {true, "test_zh.go"}, 15 | {false, "test_zh_cn.go"}, 16 | {false, "test_CN.go"}, 17 | } 18 | for _, tt := range tests { 19 | if got := IsNormalName(tt.name); got != tt.want { 20 | t.Errorf("IsNormalName(%q) = %v, want %v", tt.name, got, tt.want) 21 | } 22 | } 23 | } 24 | 25 | func TestContains(t *testing.T) { 26 | tests := []struct { 27 | s string 28 | sep string 29 | }{ 30 | {goarchList, "s390x"}, 31 | {goosList, "android"}, 32 | {goosList, "windows"}, 33 | } 34 | for _, tt := range tests { 35 | if !contains(tt.s, tt.sep) { 36 | t.Errorf("contains(%q,%q)", tt.s, tt.sep) 37 | } 38 | } 39 | } 40 | 41 | func TestOSArchTest(t *testing.T) { 42 | tests := []struct { 43 | name, goos, goarch string 44 | test bool 45 | }{ 46 | {"zsyscall_linux_s390x.go", "linux", "s390x", false}, 47 | {"zsyscall_linux_x.go", "", "", false}, 48 | {"doc_linux.go", "linux", "", false}, 49 | {"atomic_pointer.go", "", "", false}, 50 | {"doc_android_test.go", "android", "", true}, 51 | {"doc_android_arm_test.go", "android", "arm", true}, 52 | {"d_o_c_android_arm.go", "android", "arm", false}, 53 | } 54 | for _, tt := range tests { 55 | if goos, goarch, test := OSArchTest(tt.name); goos != tt.goos || 56 | goarch != tt.goarch || test != tt.test { 57 | t.Errorf("OSArchTest(%q) = %q,%q,%q want %q,%q,%q", 58 | goos, goarch, test, 59 | tt.goos, tt.goarch, tt.test) 60 | } 61 | } 62 | } 63 | 64 | func TestBuildForLinux(t *testing.T) { 65 | tests := []struct { 66 | want bool 67 | name string 68 | }{ 69 | {false, ""}, 70 | {false, "// no package"}, 71 | {true, "package n"}, 72 | {true, "//\npackage n"}, 73 | {false, "// +build ingore\npackage n"}, 74 | {false, "//+build ingore\npackage n"}, 75 | {false, "// +build linux3 windows\npackage n"}, 76 | {false, "// +buildlinux window\npackage n"}, 77 | {false, "// +build !linux window\npackage n"}, 78 | {true, "// +build linux window\npackage n"}, 79 | {true, "// +build window linux\npackage n"}, 80 | } 81 | for _, tt := range tests { 82 | if got := buildForLinux([]byte(tt.name)); got != tt.want { 83 | t.Errorf("buildForLinux(%q) = %v, want %v", tt.name, got, tt.want) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /docu/template.go: -------------------------------------------------------------------------------- 1 | package docu 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "go/doc" 7 | "path" 8 | "text/template" 9 | ) 10 | 11 | // Data 为模板提供执行数据. 12 | type Data struct { 13 | Docu *Docu 14 | ImportPath string // 提取到的传统 ImportPath 15 | Key string // 模板将要要处理的 16 | Ext string // 输出文件扩展名 17 | // 方便起见包含了声明类型常量 18 | IMPORT, CONST, VAR, TYPE, FUNC, METHOD, OTHER int 19 | 20 | buf bytes.Buffer // 仅供模板内部处理文本用 21 | filter func(*ast.File) bool 22 | } 23 | 24 | // NewData 返回需要自建立 Data.Docu 的 Data 实例. 25 | func NewData() *Data { 26 | return &Data{ 27 | IMPORT: ImportNum, 28 | CONST: ConstNum, 29 | VAR: VarNum, 30 | TYPE: TypeNum, 31 | FUNC: FuncNum, 32 | METHOD: MethodNum, 33 | } 34 | } 35 | 36 | func (d *Data) Parse(path string, source interface{}) (importPaths string, err error) { 37 | d.Ext = "" 38 | importPaths, err = d.Docu.Parse(path, source) 39 | if err == nil { 40 | d.ImportPath = importPaths 41 | } else { 42 | d.ImportPath = "" 43 | } 44 | return 45 | } 46 | 47 | func (d *Data) SetFilter(filter func(*ast.File) bool) { 48 | d.filter = filter 49 | } 50 | 51 | // File 返回 MergePackageFiles d.Key 的值 52 | func (d *Data) File() *ast.File { 53 | f := d.Docu.MergePackageFiles(d.Key) 54 | ClearComments(f) 55 | if d.filter != nil { 56 | d.filter(f) 57 | } 58 | return f 59 | } 60 | 61 | // Type 设置 d.Ext 62 | func (d *Data) Type(ext string) string { 63 | d.Ext = ext 64 | return "" 65 | } 66 | 67 | // Code 返回 decl 的代码, 支持 Const,Var,Type,Func 68 | func (d *Data) Code(decl ast.Decl) string { 69 | num := NodeNumber(decl) 70 | if num == FuncNum || num == MethodNum { 71 | return FuncLit(decl.(*ast.FuncDecl)) 72 | } 73 | genDecl, ok := decl.(*ast.GenDecl) 74 | if !ok { 75 | return "" 76 | } 77 | if len(genDecl.Specs) == 0 { 78 | return "" 79 | } 80 | 81 | docGroup := genDecl.Doc 82 | genDecl.Doc = nil 83 | d.buf.Truncate(0) 84 | FprintGenDecl(&d.buf, genDecl, nil) 85 | genDecl.Doc = docGroup 86 | return d.buf.String() 87 | } 88 | 89 | // Text 返回 decl 的注释, 支持 Const,Var,Type,Func 90 | func (d *Data) Text(decl ast.Decl) string { 91 | num := NodeNumber(decl) 92 | if num == FuncNum || num == MethodNum { 93 | fdecl := decl.(*ast.FuncDecl) 94 | return fdecl.Doc.Text() 95 | } 96 | if num != ConstNum && num != VarNum && num != TypeNum { 97 | return "" 98 | } 99 | genDecl := decl.(*ast.GenDecl) 100 | return genDecl.Doc.Text() 101 | } 102 | 103 | // Fold 利用 doc.ToText 对文档 text 进行折叠. 104 | func (d *Data) Fold(text string) string { 105 | d.buf.Truncate(0) 106 | doc.ToText(&d.buf, text, "", " ", 1<<32) 107 | return d.buf.String() 108 | } 109 | 110 | // FuncsMap 是默认的 template.FuncMap 111 | var FuncsMap = template.FuncMap{ 112 | "base": path.Base, 113 | "progress": TranslationProgress, 114 | "canonicalImportPaths": CanonicalImportPaths, 115 | "license": License, 116 | "nodeNum": NodeNumber, 117 | "lineWrap": LineWrapper, 118 | "identLit": DeclIdentLit, 119 | "originDoc": OriginDoc, 120 | "imports": func(file *ast.File) string { 121 | // 返回 file 的 import 代码 122 | return ImportsString(file.Imports) 123 | }, 124 | "wrap": func(text string) string { 125 | // 纯文本无前导缩进 126 | return WrapComments(text, "", 1<<32) 127 | }, 128 | "starLess": func(lit string) string { 129 | // 去掉 lit 前面的星号 130 | if lit == "" || lit[0] != '*' { 131 | return lit 132 | } 133 | return lit[1:] 134 | }, 135 | "sourceWrap": func(text string) string { 136 | // go source 风格, 前导 "// " 137 | return WrapComments(text, "// ", 1<<32) 138 | }, 139 | "decls": func(decls []ast.Decl, num int) []ast.Decl { 140 | // 返回已经排序的 decls 中指定节点类型 num 的声明 141 | if num >= OtherNum { 142 | return nil 143 | } 144 | first := -1 145 | last := len(decls) 146 | // 返回 147 | for i, decl := range decls { 148 | if decl == nil { 149 | continue 150 | } 151 | if num == NodeNumber(decl) { 152 | if first == -1 { 153 | first = i 154 | } 155 | } else if first != -1 { 156 | last = i 157 | break 158 | } 159 | } 160 | if first == -1 { 161 | return nil 162 | } 163 | return decls[first:last] 164 | }, 165 | "indexConstructor": func(decls []ast.Decl, typeLit string) int { 166 | // 未来实现了常规排序后, 不推荐使用此方法 167 | for i, n := range decls { 168 | num := NodeNumber(n) 169 | if num == FuncNum { 170 | if isConstructor(n.(*ast.FuncDecl), typeLit) { 171 | return i 172 | } 173 | } 174 | if num > FuncNum { 175 | break 176 | } 177 | } 178 | return -1 179 | }, 180 | "methods": func(decls []ast.Decl, typeLit string) []ast.Decl { 181 | // 未来实现了常规排序后, 不推荐使用此方法 182 | first := -1 183 | last := len(decls) 184 | for i, n := range decls { 185 | if n == nil { 186 | continue 187 | } 188 | num := NodeNumber(n) 189 | if num == MethodNum { 190 | lit := RecvIdentLit(n.(*ast.FuncDecl)) 191 | is := lit == typeLit || 192 | lit != "" && lit[0] == '*' && lit[1:] == typeLit 193 | 194 | if first == -1 { 195 | if is { 196 | first = i 197 | } 198 | } else if !is { 199 | last = i 200 | break 201 | } 202 | 203 | } else if first != -1 || num > MethodNum { 204 | last = i 205 | break 206 | } 207 | } 208 | if first == -1 { 209 | return nil 210 | } 211 | return decls[first:last] 212 | }, 213 | "clear": func(decls []ast.Decl, pos int) string { 214 | // 未来实现了常规排序后, 不推荐使用此方法 215 | if pos >= 0 && pos < len(decls) { 216 | decls[pos] = nil 217 | } 218 | return "" 219 | }, 220 | "trimRight": func(decls []ast.Decl) []ast.Decl { 221 | // 未来实现了常规排序后, 不推荐使用此方法 222 | for i, n := range decls { 223 | if n != nil { 224 | return decls[i:] 225 | } 226 | } 227 | return nil 228 | }, 229 | } 230 | 231 | const DefaultTemplate = MarkdownTemplate 232 | -------------------------------------------------------------------------------- /docu/testdata/code_origin.text: -------------------------------------------------------------------------------- 1 | // +build ingore 2 | 3 | // package comments 4 | package testdata 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 12 | // Wrap existing text at 80 characters. 13 | const Docu = "sql: no rows in result set" // comment Docu 14 | 15 | const Hi = 1 // comment Hi 16 | 17 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 18 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 19 | // Wrap existing text at 80 characters. 20 | // 21 | // This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 22 | const ( 23 | _ = iota 24 | 25 | // ok 26 | Ok // is ok 27 | 28 | // regular file 29 | TypeReg = '0' // regular file 30 | TypeLink = '1' // hard link 31 | 32 | // Tabs are reserved between the lines 33 | Link1 = '1' // 1 34 | Link2 = "333333" // 3 35 | Want // comments 36 | ) 37 | 38 | var One, Two int = 1, 2 39 | 40 | var ( 41 | Three, Four int = 3, 4 42 | Five, Six = 5, 6 43 | ) 44 | 45 | type ( 46 | // CSS encapsulates known safe content that matches any of: 47 | // 48 | // 1. This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 49 | CSS string // CSS encapsulates 50 | 51 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 52 | // Wrap existing text at 80 characters. 53 | Wrap struct { 54 | // Name of .... 55 | Name, Err string // Name 56 | 57 | // Origin 58 | Text string 59 | } 60 | 61 | // Error describes a problem encountered during template Escaping. 62 | Error struct { 63 | // ErrorCode describes the kind of error. 64 | ErrorCode ErrorCode 65 | Name string 66 | Line int 67 | 68 | // Description is a human-readable description of the problem. 69 | Description string 70 | Call func(string) string // comments 71 | } 72 | ) 73 | 74 | // The Interface interface identifies a run time error. 75 | type Interface interface { 76 | error 77 | fmt.Stringer 78 | 79 | // RuntimeError is a no-op function but 80 | // serves to distinguish types that are run time 81 | // errors from ordinary errors: a type is a 82 | // run time error if it has a RuntimeError method. 83 | RuntimeError(t1, v t2) (int, error) 84 | } 85 | 86 | type Temp Template // comments Temp 87 | 88 | // comments 89 | type Template struct { 90 | Temp 91 | } 92 | 93 | func Call(a, b string, c int, d ...bool) (e, f string) 94 | 95 | func (e Error) Error() string -------------------------------------------------------------------------------- /docu/testdata/merge_origin_trans.text: -------------------------------------------------------------------------------- 1 | // +build ingore 2 | 3 | // package comments 4 | 5 | // package comments trans 6 | package testdata 7 | 8 | import ( 9 | "fmt" 10 | "runtime" 11 | ) 12 | 13 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 14 | // Wrap existing text at 80 characters. 15 | const Docu = "sql: no rows in result set" // comment Docu 16 | 17 | const Hi = 1 // comment Hi 18 | 19 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 20 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 21 | // Wrap existing text at 80 characters. 22 | // 23 | // This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 24 | 25 | // 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 26 | // 列折行. 27 | const ( 28 | _ = iota 29 | 30 | // ok 31 | Ok // is ok 32 | 33 | // regular file 34 | 35 | // 普通文件 36 | TypeReg = '0' // regular file // 普通文件 37 | TypeLink = '1' // hard link // 硬链接 38 | 39 | // Tabs are reserved between the lines 40 | 41 | // 行间的制表符 被保留 42 | Link1 = '1' // 1 43 | Link2 = "333333" // 3 44 | Want // comments 45 | ) 46 | 47 | var One, Two int = 1, 2 48 | 49 | var ( 50 | Three, Four int = 3, 4 51 | Five, Six = 5, 6 52 | ) 53 | 54 | type ( 55 | // CSS encapsulates known safe content that matches any of: 56 | // 57 | // 1. This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 58 | 59 | // CSS用于包装匹配如下任一条的已知安全的内容: 60 | // 61 | // 1. 此行应该保持原样,不折行. 此行应该保持原样,不折行. tab 不转空格 tab 不转空格 62 | CSS string // CSS encapsulates // CSS3 样式表 63 | 64 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 65 | // Wrap existing text at 80 characters. 66 | 67 | // 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 68 | // 80 列折行. 69 | Wrap struct { 70 | // Name of .... 71 | 72 | // 此翻译把 Name 和 Err 分开写了 73 | Name, Err string // Name // 结构差异 74 | 75 | // Origin 76 | Text string 77 | } 78 | 79 | // Error describes a problem encountered during template Escaping. 80 | 81 | // Error 描述在模板转义时出现的错误。 82 | Error struct { 83 | // ErrorCode describes the kind of error. 84 | 85 | // ErrorCode 86 | ErrorCode ErrorCode 87 | Name string 88 | Line int 89 | 90 | // Description is a human-readable description of the problem. 91 | 92 | // Description 是人类可读的问题描述。 93 | Description string 94 | Call func(string) string // comments 95 | } 96 | ) 97 | 98 | // The Interface interface identifies a run time error. 99 | 100 | // Interface 接口用于标识运行时错误。 101 | type Interface interface { 102 | error 103 | fmt.Stringer 104 | 105 | // RuntimeError is a no-op function but 106 | // serves to distinguish types that are run time 107 | // errors from ordinary errors: a type is a 108 | // run time error if it has a RuntimeError method. 109 | 110 | // RuntimeError 是一个无操作函数,它只用于区分是运行时错误还是一般错误: 111 | // 若一个类型拥有 RuntimeError 方法,它就是运行时错误。 112 | RuntimeError(t1, v t2) (int, error) 113 | } 114 | 115 | type Temp Template // comments Temp 116 | 117 | // comments 118 | type Template struct { 119 | Temp 120 | } 121 | 122 | func Call(a, b string, c int, d ...bool) (e, f string) 123 | 124 | func (e Error) Error() string -------------------------------------------------------------------------------- /docu/testdata/origin.go: -------------------------------------------------------------------------------- 1 | // package comments 2 | package testdata 3 | 4 | import ( 5 | "fmt" 6 | "runtime" 7 | ) 8 | 9 | var One, Two int = 1, 2 10 | 11 | var ( 12 | Three, Four int = 3, 4 13 | Five, Six = 5, 6 14 | ) 15 | 16 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. Wrap existing text at 80 characters. 17 | const Docu = "sql: no rows in result set" // comment Docu 18 | 19 | const Hi = 1 // comment Hi 20 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 21 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. Wrap existing text at 80 characters. 22 | // This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 23 | const ( // this comment is discarded 24 | _ = iota 25 | // ok 26 | Ok // is ok 27 | // regular file 28 | TypeReg = '0' // regular file 29 | TypeLink = '1' // hard link 30 | // Tabs are reserved between the lines 31 | Link1 = '1' // 1 32 | Link2 = "333333" // 3 33 | _ 34 | Want // comments 35 | ) 36 | 37 | // The Interface interface identifies a run time error. 38 | type Interface interface { 39 | error 40 | fmt.Stringer 41 | // RuntimeError is a no-op function but 42 | // serves to distinguish types that are run time 43 | // errors from ordinary errors: a type is a 44 | // run time error if it has a RuntimeError method. 45 | RuntimeError(t1, v t2) (int, error) 46 | } 47 | 48 | // comments 49 | type Template struct { // this comment is discarded 50 | Temp 51 | } 52 | 53 | type Temp Template // comments Temp 54 | 55 | type ( 56 | // CSS encapsulates known safe content that matches any of: 57 | // 58 | // 1. This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 59 | CSS string // CSS encapsulates 60 | 61 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. Wrap existing text at 80 characters. 62 | Wrap struct { // this comment is discarded 63 | // Name of .... 64 | Name, Err string // Name 65 | // Origin 66 | Text string 67 | } 68 | 69 | // Error describes a problem encountered during template Escaping. 70 | Error struct { 71 | // ErrorCode describes the kind of error. 72 | ErrorCode ErrorCode 73 | Name string 74 | Line int 75 | // Description is a human-readable description of the problem. 76 | Description string 77 | Call func( 78 | string, 79 | ) string // comments 80 | } 81 | ) 82 | 83 | func Call(a, b string, c int, d ...bool) (e, f string) 84 | 85 | func (e Error) Error() string { 86 | return fmt.Sprint(runtime.GOOS) 87 | } 88 | -------------------------------------------------------------------------------- /docu/testdata/replace_source.text: -------------------------------------------------------------------------------- 1 | // +build ingore 2 | 3 | // package comments 4 | package testdata 5 | 6 | const ( 7 | // origin comments 8 | 9 | // trans comments 10 | Ok = 1 11 | ) 12 | 13 | type ( 14 | // origin comments. 15 | Wrap struct { 16 | // origin comments 17 | 18 | // trans comments 19 | Text string 20 | } 21 | ) 22 | 23 | type Temp Template // origin comments // trans comments 24 | 25 | // origin comments 26 | 27 | // trans comments 28 | type Template struct { 29 | } 30 | -------------------------------------------------------------------------------- /docu/testdata/replace_source_merge_origin_trans.text: -------------------------------------------------------------------------------- 1 | // +build ingore 2 | 3 | // package comments 4 | 5 | // package comments trans 6 | package testdata 7 | 8 | import ( 9 | "fmt" 10 | "runtime" 11 | ) 12 | 13 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 14 | // Wrap existing text at 80 characters. 15 | const Docu = "sql: no rows in result set" // comment Docu 16 | 17 | const Hi = 1 // comment Hi 18 | 19 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 20 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 21 | // Wrap existing text at 80 characters. 22 | // 23 | // This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 24 | 25 | // 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 26 | // 列折行. 27 | const ( 28 | _ = iota 29 | 30 | // ok 31 | 32 | // trans comments 33 | Ok // is ok 34 | 35 | // regular file 36 | 37 | // 普通文件 38 | TypeReg = '0' // regular file // 普通文件 39 | TypeLink = '1' // hard link // 硬链接 40 | 41 | // Tabs are reserved between the lines 42 | 43 | // 行间的制表符 被保留 44 | Link1 = '1' // 1 45 | Link2 = "333333" // 3 46 | Want // comments 47 | ) 48 | 49 | var One, Two int = 1, 2 50 | 51 | var ( 52 | Three, Four int = 3, 4 53 | Five, Six = 5, 6 54 | ) 55 | 56 | type ( 57 | // CSS encapsulates known safe content that matches any of: 58 | // 59 | // 1. This line should be kept as it is, is not wrapped. tab not to spaces tab not to spaces 60 | 61 | // CSS用于包装匹配如下任一条的已知安全的内容: 62 | // 63 | // 1. 此行应该保持原样,不折行. 此行应该保持原样,不折行. tab 不转空格 tab 不转空格 64 | CSS string // CSS encapsulates // CSS3 样式表 65 | 66 | // Wrap existing text at 80 characters. Wrap existing text at 80 characters. 67 | // Wrap existing text at 80 characters. 68 | 69 | // 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 70 | // 80 列折行. 71 | Wrap struct { 72 | // Name of .... 73 | 74 | // 此翻译把 Name 和 Err 分开写了 75 | Name, Err string // Name // 结构差异 76 | 77 | // Origin 78 | 79 | // trans comments 80 | Text string 81 | } 82 | 83 | // Error describes a problem encountered during template Escaping. 84 | 85 | // Error 描述在模板转义时出现的错误。 86 | Error struct { 87 | // ErrorCode describes the kind of error. 88 | 89 | // ErrorCode 90 | ErrorCode ErrorCode 91 | Name string 92 | Line int 93 | 94 | // Description is a human-readable description of the problem. 95 | 96 | // Description 是人类可读的问题描述。 97 | Description string 98 | Call func(string) string // comments 99 | } 100 | ) 101 | 102 | // The Interface interface identifies a run time error. 103 | 104 | // Interface 接口用于标识运行时错误。 105 | type Interface interface { 106 | error 107 | fmt.Stringer 108 | 109 | // RuntimeError is a no-op function but 110 | // serves to distinguish types that are run time 111 | // errors from ordinary errors: a type is a 112 | // run time error if it has a RuntimeError method. 113 | 114 | // RuntimeError 是一个无操作函数,它只用于区分是运行时错误还是一般错误: 115 | // 若一个类型拥有 RuntimeError 方法,它就是运行时错误。 116 | RuntimeError(t1, v t2) (int, error) 117 | } 118 | 119 | type Temp Template // origin comments // trans comments 120 | 121 | // comments 122 | 123 | // trans comments 124 | type Template struct { 125 | Temp 126 | } 127 | 128 | func Call(a, b string, c int, d ...bool) (e, f string) 129 | 130 | func (e Error) Error() string -------------------------------------------------------------------------------- /docu/testdata/trans.go: -------------------------------------------------------------------------------- 1 | // package comments trans 2 | package testdata 3 | 4 | // 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 5 | const ( 6 | _ = iota 7 | // ok 8 | Ok // is ok 9 | // 普通文件 10 | TypeReg = '0' // regular file // 普通文件 11 | TypeLink = '1' // hard link // 硬链接 12 | // 行间的制表符 被保留 13 | Link1 = '1' // 1 14 | Link2 = "333333" // 3 15 | ) 16 | 17 | // Interface 接口用于标识运行时错误。 18 | type Interface interface { 19 | error 20 | // RuntimeError 是一个无操作函数,它只用于区分是运行时错误还是一般错误: 21 | // 若一个类型拥有 RuntimeError 方法,它就是运行时错误。 22 | RuntimeError() 23 | } 24 | 25 | // comments 26 | type Template struct { // 该尾注释被丢弃 27 | } 28 | 29 | type ( 30 | // CSS用于包装匹配如下任一条的已知安全的内容: 31 | // 32 | // 1. 此行应该保持原样,不折行. 此行应该保持原样,不折行. tab 不转空格 tab 不转空格 33 | CSS string // CSS encapsulates // CSS3 样式表 34 | 35 | // 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 超 80 列折行. 36 | Wrap struct { // 该尾注释被丢弃 37 | // 此翻译把 Name 和 Err 分开写了 38 | Name string // Name // 结构差异 39 | Err string 40 | } 41 | 42 | // Error 描述在模板转义时出现的错误。 43 | Error struct { 44 | // ErrorCode 45 | ErrorCode ErrorCode 46 | Name string 47 | Line int 48 | // Description 是人类可读的问题描述。 49 | Description string 50 | Call func(string) string 51 | } 52 | ) 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "go/ast" 12 | "go/doc" 13 | "io" 14 | "io/ioutil" 15 | "log" 16 | "os" 17 | "path/filepath" 18 | "strings" 19 | "text/template" 20 | 21 | "github.com/golang-china/godocu/docu" 22 | ) 23 | 24 | const mode = ast.FilterFuncDuplicates | 25 | ast.FilterUnassociatedComments | ast.FilterImportDuplicates 26 | 27 | const usage = `Usage: 28 | 29 | godocu command [arguments] source [target] 30 | 31 | The commands are: 32 | 33 | diff compare the source and target, all difference output 34 | first compare the source and target, the first difference output 35 | tree compare different directory structure of the source and target 36 | code prints a formatted string to target as Go source code 37 | tmpl prints documentation from template 38 | list generate godocu style documents list 39 | merge merge source doc to target 40 | replace replace the target untranslated section in source translated section 41 | 42 | The source are: 43 | 44 | package import path or absolute path 45 | the path to a Go source file 46 | 47 | The target are: 48 | 49 | the directory as an absolute base path for compare or prints 50 | 51 | The arguments are: 52 | 53 | -file string 54 | template file for tmpl 55 | -gopath string 56 | specifies GOPATH (default $GOPATH) 57 | -goroot string 58 | specifies GOROOT (default $GOROOT) 59 | -lang string 60 | the lang pattern for the output file, form like en or zh_CN 61 | -p string 62 | package filtering, "package"|"main"|"test" (default "package") 63 | -u 64 | show unexported symbols as well as exported 65 | ` 66 | 67 | func flagUsage(err string) { 68 | fmt.Fprintln(os.Stderr, usage) 69 | if err != "" { 70 | log.Fatal(err) 71 | } 72 | os.Exit(2) 73 | } 74 | 75 | func flagParse() (command, source, target, lib, lang, file string, u bool) { 76 | var gopath string 77 | flag.StringVar(&file, "file", "", "") 78 | flag.StringVar(&docu.GOROOT, "goroot", docu.GOROOT, "") 79 | flag.StringVar(&gopath, "gopath", os.Getenv("GOPATH"), "") 80 | flag.StringVar(&lang, "lang", "", "") 81 | flag.StringVar(&lib, "p", "package", "") 82 | flag.BoolVar(&u, "u", false, "") 83 | 84 | if len(os.Args) < 3 { 85 | flagUsage("") 86 | } 87 | 88 | args := make([]string, len(os.Args[2:])) 89 | j := 0 90 | for i := 2; i < len(os.Args); i++ { 91 | if os.Args[i][0] == '-' && os.Args[i] != "--" { 92 | args[j] = os.Args[i] 93 | j++ 94 | } 95 | } 96 | 97 | for i := 2; i < len(os.Args); i++ { 98 | if os.Args[i][0] != '-' || os.Args[i] == "--" { 99 | args[j] = os.Args[i] 100 | j++ 101 | } 102 | } 103 | 104 | err := flag.CommandLine.Parse(args) 105 | if err != nil { 106 | flagUsage(err.Error()) 107 | } 108 | const libs = "package test main " 109 | if pos := strings.Index(libs, lib); pos == -1 || libs[pos+len(lib)] != ' ' { 110 | flagUsage("-p must be one of package,test,main. but got" + lib) 111 | } 112 | 113 | args = flag.Args() 114 | 115 | if len(args) == 0 || len(args) > 2 { 116 | flagUsage("") 117 | } 118 | command = os.Args[1] 119 | 120 | source = args[0] 121 | if len(args) == 2 { 122 | target = args[1] 123 | } 124 | 125 | docu.GOROOT, err = filepath.Abs(docu.GOROOT) 126 | if err != nil { 127 | flagUsage("invalid goroot: " + err.Error()) 128 | } 129 | 130 | if gopath != os.Getenv("GOPATH") { 131 | docu.GOPATHS = filepath.SplitList(gopath) 132 | for i, path := range docu.GOPATHS { 133 | docu.GOPATHS[i], err = filepath.Abs(path) 134 | if err != nil { 135 | flagUsage("invalid gopath: " + err.Error()) 136 | } 137 | } 138 | } 139 | lang = docu.LangNormal(lang) 140 | 141 | return 142 | } 143 | 144 | // 多文档输出分割线 145 | var sp = "\n\n" + strings.Repeat("/", 80) + "\n\n" 146 | 147 | func skipOSArch(f func(string) bool) func(string) bool { 148 | return func(name string) bool { 149 | if !f(name) { 150 | return false 151 | } 152 | if docu.IsNormalName(name) { 153 | return true 154 | } 155 | goos, goarch, _ := docu.OSArchTest(name) 156 | return (goos == "" || goos == "linux") && (goarch == "" || goarch == "amd64") 157 | } 158 | } 159 | 160 | func genNameFilter(lib, lang string) func(string) bool { 161 | if lang == "" { 162 | switch lib { 163 | case "package": 164 | return skipOSArch(docu.PackageFilter) 165 | case "test": 166 | return skipOSArch(docu.TestFilter) 167 | case "main": 168 | return skipOSArch(docu.MainFilter) 169 | } 170 | panic("BUG") 171 | } 172 | switch lib { 173 | case "package": 174 | return skipOSArch(docu.GenNameFilter("doc_" + lang + ".go")) 175 | case "test": 176 | return skipOSArch(docu.GenNameFilter("test_" + lang + ".go")) 177 | case "main": 178 | return skipOSArch(docu.GenNameFilter("main_" + lang + ".go")) 179 | } 180 | panic("BUG") 181 | } 182 | 183 | func genFileName(lib, lang, ext string) string { 184 | if lang == "" { 185 | return "" 186 | } 187 | switch lib { 188 | case "package": 189 | return "doc_" + lang + ext 190 | case "test": 191 | return "test_" + lang + ext 192 | case "main": 193 | return "main_" + lang + ext 194 | } 195 | panic("BUG") 196 | } 197 | 198 | func createFile(path, name string) (file *os.File, err error) { 199 | if name == "" || path == "" { 200 | return os.Stdout, nil 201 | } 202 | if err = os.MkdirAll(path, 0777); err == nil { 203 | file, err = os.OpenFile(filepath.Join(path, name), 204 | os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 205 | } 206 | return 207 | } 208 | 209 | func main() { 210 | const cmds = "code tmpl list diff first tree merge replace " 211 | var err error 212 | var info os.FileInfo 213 | command, source, target, lib, lang, file, u := flagParse() 214 | 215 | pos := strings.Index(cmds, command) 216 | if pos == -1 || cmds[pos+len(command)] != ' ' || target == "" && pos > 14 { 217 | 218 | fmt.Fprintln(os.Stderr, usage) 219 | log.Fatal("invalid command or target") 220 | } 221 | 222 | sub := strings.HasSuffix(source, "...") 223 | if sub { 224 | source = source[:len(source)-3] 225 | } 226 | 227 | if source == "" { 228 | source = docu.GOROOT + docu.SrcElem[:4] 229 | } else { 230 | source = docu.Abs(source) 231 | } 232 | 233 | if info, err = os.Stat(source); err != nil { 234 | flagUsage(err.Error()) 235 | } else if command == "tree" && !info.IsDir() { 236 | flagUsage("source must be existing directory") 237 | } 238 | 239 | // 计算导入路径的偏移量 240 | offset := posForImport(source) 241 | if offset == -1 { 242 | flagUsage("invalid source: " + source) 243 | } else if target == "--" { 244 | // 同目录输出 245 | target = source[:offset] 246 | } else { 247 | target = docu.Abs(target) 248 | } 249 | if command == "tree" { 250 | sub = true 251 | if info, err = os.Stat(target); err != nil || !info.IsDir() { 252 | flagUsage("target must be existing directory") 253 | } 254 | } 255 | 256 | ch := make(chan interface{}) 257 | go walkPath(ch, sub, source) 258 | 259 | switch command { 260 | case "code": 261 | err = codeMode(ch, offset, target, lib, lang, u) 262 | case "tree": 263 | // 对比目录结构 264 | var d1 bool 265 | prefix := fmt.Sprintf("source: %s\ntarget: %s\n\nsource target path\n", 266 | source, target) 267 | 268 | d1, err = treeMode(prefix, " path none ", " path file ", ch, source, target) 269 | if err != nil { 270 | break 271 | } 272 | close(ch) 273 | 274 | if d1 { 275 | prefix = "" 276 | } 277 | 278 | if offset < len(source) { 279 | target = filepath.Join(target, source[offset:]) 280 | source = source[:offset] 281 | } 282 | 283 | ch = make(chan interface{}) 284 | go walkPath(ch, sub, target) 285 | _, err = treeMode(prefix, " none path ", " file path ", ch, target, source) 286 | case "first", "diff": 287 | err = diffMode(command, ch, offset, target, lib, lang, u) 288 | case "merge": 289 | err = mergeMode(ch, offset, target, lib, lang) 290 | case "replace": 291 | err = replaceMode(ch, offset, target, lib, lang) 292 | case "list": 293 | err = listMode(ch, offset, target, lib, lang) 294 | case "tmpl": 295 | tpl := template.New("Godocu").Funcs(docu.FuncsMap) 296 | if file != "" { 297 | tpl, err = tpl.ParseFiles(file) 298 | } else { 299 | tpl, err = tpl.Parse(docu.DefaultTemplate) 300 | } 301 | 302 | if err != nil { 303 | <-ch 304 | break 305 | } 306 | err = tmplMode(tpl, ch, offset, target, lib, lang, u) 307 | } 308 | 309 | close(ch) 310 | if err != nil { 311 | log.Fatal(err) 312 | } 313 | } 314 | 315 | // 模板 316 | func tmplMode(tmpl *template.Template, ch chan interface{}, 317 | offset int, target, lib, lang string, u bool) (err error) { 318 | 319 | var buf bytes.Buffer 320 | 321 | var ok bool 322 | var key, source, dst string 323 | var paths string 324 | var output *os.File 325 | 326 | out := false 327 | du := docu.NewData() 328 | du.Docu = docu.New() 329 | du.Docu.Filter = genNameFilter(lib, "") 330 | 331 | tu := docu.New() 332 | if target != "" { 333 | tu.Filter = genNameFilter(lib, lang) 334 | } 335 | 336 | for i := <-ch; i != nil; i = <-ch { 337 | if err, ok = i.(error); !ok { 338 | source = i.(string) 339 | paths, err = du.Parse(source, nil) 340 | } 341 | if err != nil { 342 | break 343 | } 344 | if len(paths) == 0 { 345 | ch <- nil 346 | continue 347 | } 348 | 349 | key = paths 350 | 351 | if !u { 352 | if target != "" { 353 | // 计算目标路径, 第一个可能是单文件 354 | if dst == "" && strings.HasSuffix(source, ".go") { 355 | source = filepath.Dir(source) 356 | } 357 | dst = filepath.Join(target, source[offset:]) 358 | 359 | // 以目标过滤源 360 | paths, err = tu.Parse(dst, nil) 361 | if os.IsNotExist(err) { 362 | err = nil 363 | } 364 | if err != nil { 365 | break 366 | } 367 | dis := tu.MergePackageFiles(key) 368 | if dis != nil && paths == key { 369 | du.SetFilter(docu.SortDecl(dis.Decls).Filter) 370 | } else { 371 | du.SetFilter(docu.ExportedFileFilter) 372 | } 373 | } else { 374 | du.SetFilter(docu.ExportedFileFilter) 375 | } 376 | } 377 | 378 | buf.Truncate(0) 379 | 380 | du.Key = key 381 | if err = tmpl.Execute(&buf, du); err != nil { 382 | break 383 | } 384 | 385 | if du.Ext == "" { 386 | continue 387 | } 388 | 389 | output, err = createFile(dst, genFileName(lib, lang, du.Ext)) 390 | if err != nil { 391 | break 392 | } 393 | 394 | if out && target == "" { 395 | _, err = os.Stdout.WriteString(sp) 396 | } 397 | out = true 398 | if err == nil { 399 | _, err = output.Write(buf.Bytes()) 400 | } 401 | if output != os.Stdout { 402 | output.Close() 403 | } 404 | 405 | if err != nil { 406 | break 407 | } 408 | 409 | ch <- nil 410 | } 411 | return 412 | } 413 | 414 | // walkPath 通道类型约定: 415 | // nil 结束 416 | // string 待处理绝对路径 417 | // error 处理错误 418 | func walkPath(ch chan interface{}, sub bool, source string) { 419 | if strings.HasSuffix(source, ".go") { 420 | ch <- source 421 | <-ch 422 | ch <- nil 423 | return 424 | } 425 | docu.WalkPath(source, func(path string, _ os.FileInfo, err error) error { 426 | if err == nil { 427 | ch <- path 428 | } else { 429 | ch <- err 430 | } 431 | i := <-ch 432 | 433 | if i != nil || !sub || err != nil { 434 | return io.EOF 435 | } 436 | return nil 437 | }) 438 | ch <- nil 439 | } 440 | 441 | func codeMode(ch chan interface{}, 442 | offset int, target, lib, lang string, u bool) (err error) { 443 | 444 | var ok bool 445 | var key, source, dst string 446 | var paths string 447 | 448 | output := os.Stdout 449 | 450 | out := false 451 | du := docu.New() 452 | du.Filter = genNameFilter(lib, "") 453 | 454 | tu := docu.New() 455 | if target != "" { 456 | tu.Filter = genNameFilter(lib, lang) 457 | } 458 | 459 | fname := genFileName(lib, lang, ".go") 460 | 461 | for i := <-ch; i != nil; i = <-ch { 462 | if err, ok = i.(error); !ok { 463 | source = i.(string) 464 | paths, err = du.Parse(source, nil) 465 | } 466 | 467 | if docu.IsMultiplePkgError(err) { 468 | err = nil 469 | ch <- nil 470 | continue 471 | } 472 | 473 | if err != nil { 474 | break 475 | } 476 | if len(paths) == 0 { 477 | ch <- nil 478 | continue 479 | } 480 | 481 | key = paths 482 | file := du.MergePackageFiles(key) 483 | file.Unresolved = nil 484 | if target != "" { 485 | // 计算目标路径, 第一个可能是单文件 486 | if dst == "" && strings.HasSuffix(source, ".go") { 487 | source = filepath.Dir(source) 488 | } 489 | dst = filepath.Join(target, source[offset:]) 490 | } 491 | 492 | if !u { 493 | if target != "" { 494 | // 以目标过滤源 495 | paths, err = tu.Parse(dst, nil) 496 | if os.IsNotExist(err) { 497 | err = nil 498 | } 499 | if err != nil { 500 | break 501 | } 502 | dis := tu.MergePackageFiles(key) 503 | if dis != nil && paths == key { 504 | docu.SortDecl(dis.Decls).Filter(file) 505 | } else { 506 | docu.ExportedFileFilter(file) 507 | } 508 | 509 | // 自动提取第一个 lang, 只是为了过滤 510 | if dis != nil && lang == "" { 511 | lang = tu.NormalLang(key) 512 | tu.Filter = genNameFilter(lib, tu.NormalLang(key)) 513 | lang = "." 514 | } 515 | 516 | } else { 517 | docu.ExportedFileFilter(file) 518 | } 519 | } 520 | if target != "" && lang != "" && lang != "." { 521 | output, err = createFile(dst, fname) 522 | } 523 | if err != nil { 524 | break 525 | } 526 | 527 | if out && target == "" { 528 | _, err = os.Stdout.WriteString(sp) 529 | } 530 | out = true 531 | if err == nil { 532 | err = docu.Fprint(output, file) 533 | } 534 | 535 | if output != os.Stdout { 536 | output.Close() 537 | } 538 | 539 | if err != nil { 540 | break 541 | } 542 | 543 | ch <- nil 544 | } 545 | return 546 | } 547 | 548 | func diffMode(command string, ch chan interface{}, 549 | offset int, target, lib, lang string, u bool) (err error) { 550 | 551 | var ok, diff bool 552 | var key, source string 553 | var paths string 554 | var output *os.File 555 | 556 | fileDiff := docu.FirstDiff 557 | if command == "diff" { 558 | fileDiff = docu.Diff 559 | } 560 | du, tu := docu.New(), docu.New() 561 | du.Filter = genNameFilter(lib, "") 562 | tu.Filter = genNameFilter(lib, lang) 563 | 564 | for i := <-ch; i != nil; i = <-ch { 565 | if err, ok = i.(error); !ok { 566 | source = i.(string) 567 | paths, err = du.Parse(source, nil) 568 | } 569 | if docu.IsMultiplePkgError(err) { 570 | err = nil 571 | ch <- nil 572 | continue 573 | } 574 | if err != nil { 575 | break 576 | } 577 | if len(paths) == 0 { 578 | ch <- nil 579 | continue 580 | } 581 | 582 | // 只对比相同的包. 不能有错. 583 | key = paths 584 | if strings.HasSuffix(source, ".go") { 585 | source = filepath.Dir(source) 586 | } 587 | paths, err = tu.Parse(filepath.Join(target, source[offset:]), nil) 588 | if os.IsNotExist(err) { 589 | err = nil 590 | } 591 | if err != nil { 592 | break 593 | } 594 | if len(paths) == 0 { 595 | ch <- nil 596 | continue 597 | } 598 | 599 | diff, err = docu.TextDiff(output, "package "+key, "package "+paths) 600 | 601 | if err != nil { 602 | break 603 | } 604 | 605 | if diff { 606 | ch <- nil 607 | io.WriteString(output, sp) 608 | continue 609 | } 610 | 611 | src, dis := du.MergePackageFiles(key), tu.MergePackageFiles(key) 612 | // 自动提取第一个 lang, 只是为了过滤 613 | if dis != nil && lang == "" { 614 | lang = tu.NormalLang(key) 615 | tu.Filter = genNameFilter(lib, lang) 616 | if lang == "" { 617 | lang = "." 618 | } 619 | } 620 | 621 | if lang != "." { 622 | docu.SortDecl(dis.Decls).Filter(src) 623 | } else if !u { 624 | docu.ExportedFileFilter(src) 625 | docu.ExportedFileFilter(dis) 626 | } 627 | 628 | diff, err = fileDiff(output, src, dis) 629 | if diff && err == nil { 630 | _, err = io.WriteString(output, "FROM: package "+key) 631 | } 632 | 633 | if err != nil { 634 | break 635 | } 636 | ch <- nil 637 | } 638 | return 639 | } 640 | 641 | // posForImport 计算 import paths 开始的偏移量 642 | func posForImport(s string) (pos int) { 643 | if strings.HasSuffix(s, ".go") { 644 | s = filepath.Dir(s) 645 | } 646 | if strings.HasSuffix(s, docu.SrcElem[:4]) { 647 | return len(s) + 1 648 | } 649 | 650 | pos = strings.Index(s, docu.SrcElem) 651 | if pos != -1 { 652 | pos += len(docu.SrcElem) 653 | return 654 | } 655 | for _, wh := range docu.Warehouse { 656 | pos = strings.Index(s, wh.Host) 657 | if pos == -1 { 658 | continue 659 | } 660 | if s[pos-1] == os.PathSeparator && s[pos+len(wh.Host)] == os.PathSeparator { 661 | return pos 662 | } 663 | } 664 | 665 | return -1 666 | } 667 | 668 | func treeMode(prefix, prenone, prefile string, ch chan interface{}, source, target string) (diff bool, err error) { 669 | var fi os.FileInfo 670 | pos := posForImport(source) 671 | if pos == -1 { 672 | <-ch 673 | err = errors.New("invalid path: " + source) 674 | return 675 | } 676 | output := os.Stdout 677 | for i := <-ch; i != nil; i = <-ch { 678 | err, _ = i.(error) 679 | if err != nil { 680 | break 681 | } 682 | 683 | source = i.(string) 684 | source = source[pos:] 685 | 686 | fi, err = os.Stat(filepath.Join(target, source)) 687 | if os.IsNotExist(err) { 688 | _, err = fmt.Fprintln(output, prefix+prenone, source) 689 | if err != nil { 690 | break 691 | } 692 | diff, err, prefix = true, nil, "" 693 | } else if err == nil && !fi.IsDir() { // 虽然不大能 694 | _, err = fmt.Fprintln(output, prefix+prefile, source) 695 | if err != nil { 696 | break 697 | } 698 | diff, prefix = true, "" 699 | } 700 | if err != nil { 701 | break 702 | } 703 | ch <- nil 704 | } 705 | return 706 | } 707 | 708 | func mergeMode(ch chan interface{}, 709 | offset int, target, lib, lang string) (err error) { 710 | 711 | var ok bool 712 | var key, source, dst string 713 | var paths string 714 | var output *os.File 715 | 716 | out := false 717 | du := docu.New() 718 | du.Filter = genNameFilter(lib, "") 719 | 720 | tu := docu.New() 721 | tu.Filter = genNameFilter(lib, lang) 722 | 723 | fname := genFileName(lib, lang, ".go") 724 | 725 | // 以 target 限制为过滤条件, 因此允许所有 726 | for i := <-ch; i != nil; i = <-ch { 727 | if err, ok = i.(error); !ok { 728 | source = i.(string) 729 | paths, err = du.Parse(source, nil) 730 | } 731 | // if docu.IsMultiplePkgError(err) { 732 | // err = nil 733 | // ch <- nil 734 | // continue 735 | // } 736 | if err != nil { 737 | break 738 | } 739 | if len(paths) == 0 { 740 | ch <- nil 741 | continue 742 | } 743 | 744 | key = paths 745 | 746 | // 计算目标路径, 第一个可能是单文件 747 | if dst == "" && strings.HasSuffix(source, ".go") { 748 | source = filepath.Dir(source) 749 | } 750 | dst = filepath.Join(target, source[offset:]) 751 | 752 | paths, err = tu.Parse(filepath.Join(dst, fname), nil) 753 | if os.IsNotExist(err) { 754 | err = nil 755 | } 756 | if err != nil { 757 | break 758 | } 759 | if key != paths { 760 | ch <- nil 761 | continue 762 | } 763 | dis := tu.MergePackageFiles(key) 764 | 765 | // 自动提取第一个 lang, 只是为了过滤 766 | if lang == "" { 767 | lang = tu.NormalLang(key) 768 | if lang == "" { 769 | err = errors.New("missing argument lang") 770 | break 771 | } 772 | tu.Filter = genNameFilter(lib, lang) 773 | output = os.Stdout 774 | fname = genFileName(lib, lang, ".go") 775 | lang = "." 776 | } 777 | 778 | src := du.MergePackageFiles(key) 779 | 780 | // src 为输出结果, 用目标过滤源 781 | docu.SortDecl(dis.Decls).Filter(src) 782 | 783 | if !docu.EqualComment(src.Doc, dis.Doc) { 784 | docu.MergeDoc(dis.Doc, src.Doc) 785 | } 786 | 787 | docu.MergeDeclsDoc(dis.Decls, src.Decls) 788 | 789 | if lang != "." { 790 | output, err = createFile(dst, fname) 791 | } 792 | 793 | if err != nil { 794 | break 795 | } 796 | 797 | if lang == "." { 798 | if out { 799 | _, err = os.Stdout.WriteString(sp) 800 | } 801 | out = true 802 | } 803 | 804 | if err == nil { 805 | src.Unresolved = nil // 防止万一 src 为 godocu 806 | err = docu.Fprint(output, src) 807 | } 808 | if output != os.Stdout { 809 | output.Close() 810 | } 811 | 812 | if err != nil { 813 | break 814 | } 815 | ch <- nil 816 | } 817 | return 818 | } 819 | 820 | func replaceMode(ch chan interface{}, 821 | offset int, target, lib, lang string) (err error) { 822 | 823 | var ok bool 824 | var key, source, dst string 825 | var paths string 826 | 827 | output := os.Stdout 828 | 829 | out := false 830 | du := docu.New() 831 | du.Filter = genNameFilter(lib, lang) 832 | 833 | tu := docu.New() 834 | tu.Filter = du.Filter 835 | 836 | fname := genFileName(lib, lang, ".go") 837 | 838 | // 以 target 限制为过滤条件, 因此允许所有 839 | for i := <-ch; i != nil; i = <-ch { 840 | if err, ok = i.(error); !ok { 841 | source = i.(string) 842 | paths, err = du.Parse(source, nil) 843 | } 844 | // if docu.IsMultiplePkgError(err) { 845 | // err = nil 846 | // ch <- nil 847 | // continue 848 | // } 849 | if err != nil { 850 | break 851 | } 852 | if len(paths) == 0 { 853 | ch <- nil 854 | continue 855 | } 856 | 857 | key = paths 858 | 859 | // 计算目标路径, 第一个可能是单文件 860 | if dst == "" && strings.HasSuffix(source, ".go") { 861 | source = filepath.Dir(source) 862 | } 863 | dst = filepath.Join(target, source[offset:]) 864 | 865 | paths, err = tu.Parse(filepath.Join(dst, fname), nil) 866 | if os.IsNotExist(err) { 867 | err = nil 868 | } 869 | if err != nil { 870 | break 871 | } 872 | dis := tu.MergePackageFiles(key) 873 | if dis == nil || paths != key { 874 | ch <- nil 875 | continue 876 | } 877 | 878 | src := du.MergePackageFiles(key) 879 | if !docu.IsGodocuFile(src) || !docu.IsGodocuFile(dis) { 880 | err = errors.New("source and target must be GodocuStyle documents") 881 | break 882 | } 883 | 884 | // 自动提取第一个 lang, 只是为了过滤 885 | if lang == "" { 886 | lang = du.NormalLang(key) 887 | if lang == "" || lang != tu.NormalLang(key) { 888 | err = errors.New("missing argument lang") 889 | break 890 | } 891 | du.Filter = genNameFilter(lib, lang) 892 | tu.Filter = du.Filter 893 | fname = genFileName(lib, lang, ".go") 894 | lang = "." 895 | } 896 | 897 | docu.Replace(dis, src) 898 | 899 | if len(dis.Imports) == 0 { 900 | dis.Imports = src.Imports 901 | } 902 | 903 | if lang != "." { 904 | output, err = createFile(dst, fname) 905 | } 906 | 907 | if err != nil { 908 | break 909 | } 910 | 911 | if lang == "." { 912 | if out { 913 | _, err = os.Stdout.WriteString(sp) 914 | } 915 | out = true 916 | } 917 | 918 | if err == nil { 919 | err = docu.Fprint(output, dis) 920 | } 921 | 922 | if output != os.Stdout { 923 | output.Close() 924 | } 925 | 926 | if err != nil { 927 | break 928 | } 929 | ch <- nil 930 | } 931 | return 932 | } 933 | 934 | func listMode(ch chan interface{}, offset int, target, lib, lang string) (err error) { 935 | var ok bool 936 | var source string 937 | var paths string 938 | var list docu.List 939 | var bs []byte 940 | 941 | du := docu.New() 942 | du.Filter = genNameFilter(lib, lang) 943 | list.Filename = genFileName(lib, lang, ".go") 944 | 945 | if target != "" { 946 | info, err := os.Stat(target) 947 | if err != nil { 948 | return err 949 | } 950 | if info.IsDir() { 951 | target = filepath.Join(target, "golist.json") 952 | } else if !strings.HasSuffix(info.Name(), ".json") { 953 | return errors.New("invalid target") 954 | } 955 | bs, _ = ioutil.ReadFile(target) 956 | // 提取现有属性 957 | if bs != nil { 958 | err = json.Unmarshal(bs, &list) 959 | if err != nil { 960 | return err 961 | } 962 | } 963 | 964 | if list.Readme == "" { 965 | list.Readme = docu.LookReadme(filepath.Dir(target)) 966 | } else { 967 | info, err := os.Lstat(filepath.Join(filepath.Dir(target), list.Readme)) 968 | if err != nil || info.IsDir() { 969 | return errors.New("invalid readme file: " + list.Readme) 970 | } 971 | } 972 | list.Package = nil 973 | if lang == "" { 974 | lang = docu.LangOf(list.Filename) 975 | list.Filename = genFileName(lib, lang, ".go") 976 | du.Filter = genNameFilter(lib, lang) 977 | lang = "." 978 | } 979 | } 980 | 981 | for i := <-ch; i != nil; i = <-ch { 982 | if err, ok = i.(error); !ok { 983 | source = i.(string) 984 | paths, err = du.Parse(source, nil) 985 | } 986 | if err != nil { 987 | break 988 | } 989 | 990 | if len(paths) == 0 { 991 | // 简单预测官方包 992 | if list.Repo == "" { 993 | if filepath.Base(source) == "archive" { 994 | list.Repo = "github.com/golang/go" 995 | } 996 | } 997 | ch <- nil 998 | continue 999 | } 1000 | key := paths 1001 | // 自动提取第一个 lang, 只是为了过滤 1002 | if lang == "" { 1003 | lang = du.NormalLang(key) 1004 | list.Filename = genFileName(lib, lang, ".go") 1005 | du.Filter = genNameFilter(lib, lang) 1006 | lang = "." 1007 | } 1008 | 1009 | file := du.MergePackageFiles(key) 1010 | 1011 | info := docu.Info{ 1012 | Synopsis: doc.Synopsis(file.Doc.Text()), 1013 | Progress: docu.TranslationProgress(file), 1014 | Readme: docu.LookReadme(source), 1015 | Import: source[offset:], 1016 | } 1017 | 1018 | list.Package = append(list.Package, info) 1019 | 1020 | if list.Repo == "" { 1021 | // 官方包引向 github, 其它引向 "localhost" 1022 | imp := info.Import 1023 | if strings.HasPrefix(imp, "golang.org/x") { 1024 | list.Repo = "github.com/golang/tools" 1025 | } else if pos := strings.IndexByte(imp, '/'); pos != -1 { 1026 | imp = imp[:pos] 1027 | } 1028 | if list.Repo == "" && strings.IndexByte(imp, '.') != -1 { 1029 | for _, wh := range docu.Warehouse { 1030 | if wh.Host != imp { 1031 | continue 1032 | } 1033 | pos := 1 1034 | for i := 0; i < wh.Part; i++ { 1035 | end := strings.IndexByte(info.Import[pos:], '/') 1036 | if end == -1 { 1037 | break 1038 | } 1039 | pos += end + 1 1040 | } 1041 | list.Repo = info.Import[:pos-1] 1042 | break 1043 | } 1044 | } 1045 | if list.Repo == "" { 1046 | list.Repo = "localhost" 1047 | } 1048 | } 1049 | 1050 | ch <- nil 1051 | } 1052 | 1053 | if err == nil { 1054 | bs, err = json.MarshalIndent(list, "", " ") 1055 | } 1056 | if err == nil { 1057 | if target == "" || lang == "." { 1058 | _, err = os.Stdout.Write(bs) 1059 | return 1060 | } 1061 | var output *os.File 1062 | output, err = os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 1063 | if err == nil { 1064 | _, err = output.Write(bs) 1065 | output.Close() 1066 | } 1067 | } 1068 | return 1069 | } 1070 | --------------------------------------------------------------------------------