Skip to content

Commit 77f2aca

Browse files
committed
chore: sync with 93e86851e9f22f1f2db57812cf71fc004c02159c
1 parent 5a18a86 commit 77f2aca

File tree

10 files changed

+641
-0
lines changed

10 files changed

+641
-0
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**No modifications will be accepted other than the synchronization of the fork.**
2+
3+
The synchronization of the fork will be done by the golangci-lint maintainers only.

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: test
2+
on:
3+
push:
4+
5+
jobs:
6+
test:
7+
strategy:
8+
fail-fast: false
9+
matrix:
10+
os:
11+
- ubuntu-latest
12+
- macos-latest
13+
- windows-latest
14+
go:
15+
- stable
16+
- oldstable
17+
runs-on: ${{ matrix.os }}
18+
name: swaggo/swag tests (using go ${{ matrix.go }} on ${{ matrix.os }})
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-go@v5
22+
with:
23+
go-version: ${{ matrix.go }}
24+
- run: go test -v ./...

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
dist
2+
testdata/simple*/docs
3+
testdata/quotes/docs
4+
testdata/quotes/quotes.so
5+
testdata/delims/docs
6+
testdata/delims/delims.so
7+
example/basic/docs/*
8+
example/celler/docs/*
9+
cover.out
10+
11+
12+
# Test binary, build with `go test -c`
13+
*.test
14+
15+
# Output of the go coverage tool, specifically when used with LiteIDE
16+
*.out
17+
.idea
18+
.vscode
19+
20+
# Etc
21+
.DS_Store
22+
23+
/swag
24+
/swag.exe

formatter.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package swaggoswag
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"go/ast"
7+
goparser "go/parser"
8+
"go/token"
9+
"regexp"
10+
"sort"
11+
"strings"
12+
"text/tabwriter"
13+
14+
"golang.org/x/tools/imports"
15+
)
16+
17+
// Check of @Param @Success @Failure @Response @Header
18+
var specialTagForSplit = map[string]bool{
19+
paramAttr: true,
20+
successAttr: true,
21+
failureAttr: true,
22+
responseAttr: true,
23+
headerAttr: true,
24+
}
25+
26+
var skipChar = map[byte]byte{
27+
'"': '"',
28+
'(': ')',
29+
'{': '}',
30+
'[': ']',
31+
}
32+
33+
// Formatter implements a formatter for Go source files.
34+
type Formatter struct{}
35+
36+
// NewFormatter create a new formatter instance.
37+
func NewFormatter() *Formatter {
38+
formatter := &Formatter{}
39+
return formatter
40+
}
41+
42+
// Format formats swag comments in contents. It uses fileName to report errors
43+
// that happen during parsing of contents.
44+
func (f *Formatter) Format(fileName string, contents []byte) ([]byte, error) {
45+
fileSet := token.NewFileSet()
46+
ast, err := goparser.ParseFile(fileSet, fileName, contents, goparser.ParseComments)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
// Formatting changes are described as an edit list of byte range
52+
// replacements. We make these content-level edits directly rather than
53+
// changing the AST nodes and writing those out (via [go/printer] or
54+
// [go/format]) so that we only change the formatting of Swag attribute
55+
// comments. This won't touch the formatting of any other comments, or of
56+
// functions, etc.
57+
maxEdits := 0
58+
for _, comment := range ast.Comments {
59+
maxEdits += len(comment.List)
60+
}
61+
edits := make(edits, 0, maxEdits)
62+
63+
for _, comment := range ast.Comments {
64+
formatFuncDoc(fileSet, comment.List, &edits)
65+
}
66+
formatted, err := imports.Process(fileName, edits.apply(contents), nil)
67+
if err != nil {
68+
return nil, err
69+
}
70+
return formatted, nil
71+
}
72+
73+
type edit struct {
74+
begin int
75+
end int
76+
replacement []byte
77+
}
78+
79+
type edits []edit
80+
81+
func (edits edits) apply(contents []byte) []byte {
82+
// Apply the edits with the highest offset first, so that earlier edits
83+
// don't affect the offsets of later edits.
84+
sort.Slice(edits, func(i, j int) bool {
85+
return edits[i].begin > edits[j].begin
86+
})
87+
88+
for _, edit := range edits {
89+
prefix := contents[:edit.begin]
90+
suffix := contents[edit.end:]
91+
contents = append(prefix, append(edit.replacement, suffix...)...)
92+
}
93+
94+
return contents
95+
}
96+
97+
// formatFuncDoc reformats the comment lines in commentList, and appends any
98+
// changes to the edit list.
99+
func formatFuncDoc(fileSet *token.FileSet, commentList []*ast.Comment, edits *edits) {
100+
// Building the edit list to format a comment block is a two-step process.
101+
// First, we iterate over each comment line looking for Swag attributes. In
102+
// each one we find, we replace alignment whitespace with a tab character,
103+
// then write the result into a tab writer.
104+
105+
linesToComments := make(map[int]int, len(commentList))
106+
107+
buffer := &bytes.Buffer{}
108+
w := tabwriter.NewWriter(buffer, 1, 4, 1, '\t', 0)
109+
110+
for commentIndex, comment := range commentList {
111+
text := comment.Text
112+
if attr, body, found := swagComment(text); found {
113+
formatted := "//\t" + attr
114+
if body != "" {
115+
formatted += "\t" + splitComment2(attr, body)
116+
}
117+
_, _ = fmt.Fprintln(w, formatted)
118+
linesToComments[len(linesToComments)] = commentIndex
119+
}
120+
}
121+
122+
// Once we've loaded all of the comment lines to be aligned into the tab
123+
// writer, flushing it causes the aligned text to be written out to the
124+
// backing buffer.
125+
_ = w.Flush()
126+
127+
// Now the second step: we iterate over the aligned comment lines that were
128+
// written into the backing buffer, pair each one up to its original
129+
// comment line, and use the combination to describe the edit that needs to
130+
// be made to the original input.
131+
formattedComments := bytes.Split(buffer.Bytes(), []byte("\n"))
132+
for lineIndex, commentIndex := range linesToComments {
133+
comment := commentList[commentIndex]
134+
*edits = append(*edits, edit{
135+
begin: fileSet.Position(comment.Pos()).Offset,
136+
end: fileSet.Position(comment.End()).Offset,
137+
replacement: formattedComments[lineIndex],
138+
})
139+
}
140+
}
141+
142+
func splitComment2(attr, body string) string {
143+
if specialTagForSplit[strings.ToLower(attr)] {
144+
for i := 0; i < len(body); i++ {
145+
if skipEnd, ok := skipChar[body[i]]; ok {
146+
skipStart, n := body[i], 1
147+
for i++; i < len(body); i++ {
148+
if skipStart != skipEnd && body[i] == skipStart {
149+
n++
150+
} else if body[i] == skipEnd {
151+
n--
152+
if n == 0 {
153+
break
154+
}
155+
}
156+
}
157+
} else if body[i] == ' ' || body[i] == '\t' {
158+
j := i
159+
for ; j < len(body) && (body[j] == ' ' || body[j] == '\t'); j++ {
160+
}
161+
body = replaceRange(body, i, j, "\t")
162+
}
163+
}
164+
}
165+
return body
166+
}
167+
168+
func replaceRange(s string, start, end int, new string) string {
169+
return s[:start] + new + s[end:]
170+
}
171+
172+
var swagCommentLineExpression = regexp.MustCompile(`^\/\/\s+(@[\S.]+)\s*(.*)`)
173+
174+
func swagComment(comment string) (string, string, bool) {
175+
matches := swagCommentLineExpression.FindStringSubmatch(comment)
176+
if matches == nil {
177+
return "", "", false
178+
}
179+
return matches[1], matches[2], true
180+
}

0 commit comments

Comments
 (0)