forked from ebhomengo/niki
1
0
Fork 0
niki/vendor/github.com/swaggo/swag/formatter.go

297 lines
5.0 KiB
Go
Raw Normal View History

2024-05-14 13:07:09 +00:00
package swag
import (
"bytes"
"fmt"
"go/ast"
goparser "go/parser"
"go/token"
"log"
"os"
"regexp"
"sort"
"strings"
"text/tabwriter"
)
// Check of @Param @Success @Failure @Response @Header
2024-05-14 13:07:09 +00:00
var specialTagForSplit = map[string]bool{
paramAttr: true,
successAttr: true,
failureAttr: true,
2024-05-14 13:07:09 +00:00
responseAttr: true,
headerAttr: true,
2024-05-14 13:07:09 +00:00
}
var skipChar = map[byte]byte{
2024-05-14 13:07:09 +00:00
'"': '"',
2024-05-14 13:07:09 +00:00
'(': ')',
2024-05-14 13:07:09 +00:00
'{': '}',
2024-05-14 13:07:09 +00:00
'[': ']',
}
// Formatter implements a formatter for Go source files.
2024-05-14 13:07:09 +00:00
type Formatter struct {
2024-05-14 13:07:09 +00:00
// debugging output goes here
2024-05-14 13:07:09 +00:00
debug Debugger
}
// NewFormatter create a new formatter instance.
2024-05-14 13:07:09 +00:00
func NewFormatter() *Formatter {
2024-05-14 13:07:09 +00:00
formatter := &Formatter{
2024-05-14 13:07:09 +00:00
debug: log.New(os.Stdout, "", log.LstdFlags),
}
2024-05-14 13:07:09 +00:00
return formatter
2024-05-14 13:07:09 +00:00
}
// Format formats swag comments in contents. It uses fileName to report errors
2024-05-14 13:07:09 +00:00
// that happen during parsing of contents.
2024-05-14 13:07:09 +00:00
func (f *Formatter) Format(fileName string, contents []byte) ([]byte, error) {
2024-05-14 13:07:09 +00:00
fileSet := token.NewFileSet()
2024-05-14 13:07:09 +00:00
ast, err := goparser.ParseFile(fileSet, fileName, contents, goparser.ParseComments)
2024-05-14 13:07:09 +00:00
if err != nil {
2024-05-14 13:07:09 +00:00
return nil, err
2024-05-14 13:07:09 +00:00
}
// Formatting changes are described as an edit list of byte range
2024-05-14 13:07:09 +00:00
// replacements. We make these content-level edits directly rather than
2024-05-14 13:07:09 +00:00
// changing the AST nodes and writing those out (via [go/printer] or
2024-05-14 13:07:09 +00:00
// [go/format]) so that we only change the formatting of Swag attribute
2024-05-14 13:07:09 +00:00
// comments. This won't touch the formatting of any other comments, or of
2024-05-14 13:07:09 +00:00
// functions, etc.
2024-05-14 13:07:09 +00:00
maxEdits := 0
2024-05-14 13:07:09 +00:00
for _, comment := range ast.Comments {
2024-05-14 13:07:09 +00:00
maxEdits += len(comment.List)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
edits := make(edits, 0, maxEdits)
for _, comment := range ast.Comments {
2024-05-14 13:07:09 +00:00
formatFuncDoc(fileSet, comment.List, &edits)
2024-05-14 13:07:09 +00:00
}
return edits.apply(contents), nil
2024-05-14 13:07:09 +00:00
}
type edit struct {
begin int
end int
2024-05-14 13:07:09 +00:00
replacement []byte
}
type edits []edit
func (edits edits) apply(contents []byte) []byte {
2024-05-14 13:07:09 +00:00
// Apply the edits with the highest offset first, so that earlier edits
2024-05-14 13:07:09 +00:00
// don't affect the offsets of later edits.
2024-05-14 13:07:09 +00:00
sort.Slice(edits, func(i, j int) bool {
2024-05-14 13:07:09 +00:00
return edits[i].begin > edits[j].begin
2024-05-14 13:07:09 +00:00
})
for _, edit := range edits {
2024-05-14 13:07:09 +00:00
prefix := contents[:edit.begin]
2024-05-14 13:07:09 +00:00
suffix := contents[edit.end:]
2024-05-14 13:07:09 +00:00
contents = append(prefix, append(edit.replacement, suffix...)...)
2024-05-14 13:07:09 +00:00
}
return contents
2024-05-14 13:07:09 +00:00
}
// formatFuncDoc reformats the comment lines in commentList, and appends any
2024-05-14 13:07:09 +00:00
// changes to the edit list.
2024-05-14 13:07:09 +00:00
func formatFuncDoc(fileSet *token.FileSet, commentList []*ast.Comment, edits *edits) {
2024-05-14 13:07:09 +00:00
// Building the edit list to format a comment block is a two-step process.
2024-05-14 13:07:09 +00:00
// First, we iterate over each comment line looking for Swag attributes. In
2024-05-14 13:07:09 +00:00
// each one we find, we replace alignment whitespace with a tab character,
2024-05-14 13:07:09 +00:00
// then write the result into a tab writer.
linesToComments := make(map[int]int, len(commentList))
buffer := &bytes.Buffer{}
2024-05-14 13:07:09 +00:00
w := tabwriter.NewWriter(buffer, 1, 4, 1, '\t', 0)
for commentIndex, comment := range commentList {
2024-05-14 13:07:09 +00:00
text := comment.Text
2024-05-14 13:07:09 +00:00
if attr, body, found := swagComment(text); found {
2024-05-14 13:07:09 +00:00
formatted := "//\t" + attr
2024-05-14 13:07:09 +00:00
if body != "" {
2024-05-14 13:07:09 +00:00
formatted += "\t" + splitComment2(attr, body)
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
_, _ = fmt.Fprintln(w, formatted)
2024-05-14 13:07:09 +00:00
linesToComments[len(linesToComments)] = commentIndex
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
// Once we've loaded all of the comment lines to be aligned into the tab
2024-05-14 13:07:09 +00:00
// writer, flushing it causes the aligned text to be written out to the
2024-05-14 13:07:09 +00:00
// backing buffer.
2024-05-14 13:07:09 +00:00
_ = w.Flush()
// Now the second step: we iterate over the aligned comment lines that were
2024-05-14 13:07:09 +00:00
// written into the backing buffer, pair each one up to its original
2024-05-14 13:07:09 +00:00
// comment line, and use the combination to describe the edit that needs to
2024-05-14 13:07:09 +00:00
// be made to the original input.
2024-05-14 13:07:09 +00:00
formattedComments := bytes.Split(buffer.Bytes(), []byte("\n"))
2024-05-14 13:07:09 +00:00
for lineIndex, commentIndex := range linesToComments {
2024-05-14 13:07:09 +00:00
comment := commentList[commentIndex]
2024-05-14 13:07:09 +00:00
*edits = append(*edits, edit{
begin: fileSet.Position(comment.Pos()).Offset,
end: fileSet.Position(comment.End()).Offset,
2024-05-14 13:07:09 +00:00
replacement: formattedComments[lineIndex],
})
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
func splitComment2(attr, body string) string {
2024-05-14 13:07:09 +00:00
if specialTagForSplit[strings.ToLower(attr)] {
2024-05-14 13:07:09 +00:00
for i := 0; i < len(body); i++ {
2024-05-14 13:07:09 +00:00
if skipEnd, ok := skipChar[body[i]]; ok {
2024-05-14 13:07:09 +00:00
skipStart, n := body[i], 1
2024-05-14 13:07:09 +00:00
for i++; i < len(body); i++ {
2024-05-14 13:07:09 +00:00
if skipStart != skipEnd && body[i] == skipStart {
2024-05-14 13:07:09 +00:00
n++
2024-05-14 13:07:09 +00:00
} else if body[i] == skipEnd {
2024-05-14 13:07:09 +00:00
n--
2024-05-14 13:07:09 +00:00
if n == 0 {
2024-05-14 13:07:09 +00:00
break
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
} else if body[i] == ' ' || body[i] == '\t' {
2024-05-14 13:07:09 +00:00
j := i
2024-05-14 13:07:09 +00:00
for ; j < len(body) && (body[j] == ' ' || body[j] == '\t'); j++ {
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
body = replaceRange(body, i, j, "\t")
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return body
2024-05-14 13:07:09 +00:00
}
func replaceRange(s string, start, end int, new string) string {
2024-05-14 13:07:09 +00:00
return s[:start] + new + s[end:]
2024-05-14 13:07:09 +00:00
}
var swagCommentLineExpression = regexp.MustCompile(`^\/\/\s+(@[\S.]+)\s*(.*)`)
func swagComment(comment string) (string, string, bool) {
2024-05-14 13:07:09 +00:00
matches := swagCommentLineExpression.FindStringSubmatch(comment)
2024-05-14 13:07:09 +00:00
if matches == nil {
2024-05-14 13:07:09 +00:00
return "", "", false
2024-05-14 13:07:09 +00:00
}
2024-05-14 13:07:09 +00:00
return matches[1], matches[2], true
2024-05-14 13:07:09 +00:00
}