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