From d7cec5730e4e9ff1c05e98b780c0092b0ad35cbe Mon Sep 17 00:00:00 2001 From: Jamie <11019755+gatorjuice@users.noreply.github.com> Date: Fri, 18 Jul 2025 01:32:30 -0500 Subject: [PATCH 1/5] fix: prevent nil pointer dereference in getFuncDoc when parsing dependencies (#2044) Add comprehensive nil checks in getFuncDoc function to handle incomplete AST nodes from dependency packages. This resolves segmentation faults that occur when parsing standard library types like atomic.Int32, json.scanner, and ecdh.PrivateKey with ParseDependency enabled. - Check for empty astDecl.Specs before accessing astDecl.Specs[0] - Check for empty astDecl.Values before accessing astDecl.Values[0] - Add nil checks for value.Obj and value.Obj.Decl chain Fixes critical crash when parsing projects with dependencies. Co-authored-by: James Gates --- parser.go | 8 +++++- parser_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/parser.go b/parser.go index 892c4691f..3e12c2fc0 100644 --- a/parser.go +++ b/parser.go @@ -1049,6 +1049,9 @@ func getFuncDoc(decl any) (*ast.CommentGroup, bool) { if astDecl.Tok != token.VAR { return nil, false } + if len(astDecl.Specs) == 0 { + return nil, false + } varSpec, ok := astDecl.Specs[0].(*ast.ValueSpec) if !ok || len(varSpec.Values) != 1 { return nil, false @@ -1056,8 +1059,11 @@ func getFuncDoc(decl any) (*ast.CommentGroup, bool) { _, ok = getFuncDoc(varSpec) return astDecl.Doc, ok case *ast.ValueSpec: + if len(astDecl.Values) == 0 { + return nil, false + } value, ok := astDecl.Values[0].(*ast.Ident) - if !ok || value == nil { + if !ok || value == nil || value.Obj == nil || value.Obj.Decl == nil { return nil, false } _, ok = getFuncDoc(value.Obj.Decl) diff --git a/parser_test.go b/parser_test.go index b0929fa17..45ef34483 100644 --- a/parser_test.go +++ b/parser_test.go @@ -3995,6 +3995,73 @@ func TestParser_Skip(t *testing.T) { assert.Error(t, parser.Skip(filepath.Clean("admin/release"), &mockFS{IsDirectory: true})) } +func TestGetFuncDoc_NilPointerSafety(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + decl interface{} + wantDoc *ast.CommentGroup + wantBool bool + }{ + { + name: "GenDecl with empty Specs", + decl: &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{}, // empty specs + }, + wantDoc: nil, + wantBool: false, + }, + { + name: "ValueSpec with empty Values", + decl: &ast.ValueSpec{ + Values: []ast.Expr{}, // empty values + }, + wantDoc: nil, + wantBool: false, + }, + { + name: "ValueSpec with nil Obj", + decl: &ast.ValueSpec{ + Values: []ast.Expr{ + &ast.Ident{ + Name: "test", + Obj: nil, // nil object + }, + }, + }, + wantDoc: nil, + wantBool: false, + }, + { + name: "ValueSpec with nil Obj.Decl", + decl: &ast.ValueSpec{ + Values: []ast.Expr{ + &ast.Ident{ + Name: "test", + Obj: &ast.Object{ + Decl: nil, // nil declaration + }, + }, + }, + }, + wantDoc: nil, + wantBool: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotDoc, gotBool := getFuncDoc(tt.decl) + assert.Equal(t, tt.wantDoc, gotDoc) + assert.Equal(t, tt.wantBool, gotBool) + }) + } +} + func TestGetFieldType(t *testing.T) { t.Parallel() From 0f3bf86377c7e1c5bbf280380419e9ded717d83a Mon Sep 17 00:00:00 2001 From: Subhash Chandran Date: Mon, 21 Jul 2025 12:56:51 +0530 Subject: [PATCH 2/5] fix: router with tilde #2004 (#2005) --- operation.go | 2 +- operation_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/operation.go b/operation.go index 7518446c5..185f823bc 100644 --- a/operation.go +++ b/operation.go @@ -713,7 +713,7 @@ func parseMimeTypeList(mimeTypeList string, typeList *[]string, format string) e return nil } -var routerPattern = regexp.MustCompile(`^(/[\w./\-{}\(\)+:$]*)[[:blank:]]+\[(\w+)]`) +var routerPattern = regexp.MustCompile(`^(/[\w./\-{}\(\)+:$~]*)[[:blank:]]+\[(\w+)]`) // ParseRouterComment parses comment for given `router` comment string. func (operation *Operation) ParseRouterComment(commentLine string, deprecated bool) error { diff --git a/operation_test.go b/operation_test.go index 0905a1e5a..98fd08ca6 100644 --- a/operation_test.go +++ b/operation_test.go @@ -217,6 +217,15 @@ func TestParseRouterCommentNoColonSignAtPathStartErr(t *testing.T) { assert.Error(t, err) } +func TestParseRouterCommentWithTilde(t *testing.T) { + t.Parallel() + + comment := `@Router /customer/{id}/~last-name [patch]` + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) +} + func TestParseRouterCommentMethodSeparationErr(t *testing.T) { t.Parallel() From b0c5cc990594386ba6189a21313e4e8e68695249 Mon Sep 17 00:00:00 2001 From: Drew Silcock Date: Thu, 24 Jul 2025 03:04:34 +0100 Subject: [PATCH 3/5] Feature: allow enum ordered const name override (2nd PR for this) (#2046) * Implement getting name override from comment for enum ordered consts. * Update README and enum tests for enum variant name override feature. * Update to address comments and linting. * Fix typo in ConstVariable.VariableName() comment. * Improve named enum testing. * `x-enum-descriptions` should have same length as `enum` array. Otherwise, there's no way to know which descriptions corresponds to which enum value. * Update TypeSpecDef.Alias() to use standard nameOverride() function. * Fix enum test. * Remove name override from TypeSpecDef.TypeName() method. As per PR #1866, the name override functionality has been removed from TypeName() and put into Alias() which is used by SetSchemaName(). --- README.md | 30 +++++++ const.go | 13 ++++ enums_test.go | 20 +++++ packages.go | 14 +--- parser.go | 2 +- schema.go | 41 ++++++++++ testdata/enums/expected.json | 142 +++++++++++++++++++++++++++++++++- testdata/enums/types/model.go | 38 ++++++--- types.go | 20 +---- 9 files changed, 277 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index fa3696766..0d485c331 100644 --- a/README.md +++ b/README.md @@ -913,6 +913,36 @@ Make it AND condition // @Security OAuth2Application[write, admin] && APIKeyAuth ``` +### Generate enum types from enum constants + +You can generate enums from ordered constants. Each enum variant can have a comment, an override name, or both. This works with both iota-defined and manually defined constants. + +```go +type Difficulty string + +const ( + Easy Difficulty = "easy" // You can add a comment to the enum variant. + Medium Difficulty = "medium" // @name MediumDifficulty + Hard Difficulty = "hard" // @name HardDifficulty You can have a name override and a comment. +) + +type Class int + +const ( + First Class = iota // @name FirstClass + Second // Name override and comment rules apply here just as above. + Third // @name ThirdClass This one has a name override and a comment. +) + +// There is no need to add `enums:"..."` to the fields, it is automatically generated from the ordered consts. +type Quiz struct { + Difficulty Difficulty + Class Class + Questions []string + Answers []string +} +``` + ### Add a description for enum items diff --git a/const.go b/const.go index 83755103b..a23d8bd7f 100644 --- a/const.go +++ b/const.go @@ -19,6 +19,19 @@ type ConstVariable struct { Pkg *PackageDefinitions } +// VariableName gets the name for this const variable, taking into account comment overrides. +func (cv *ConstVariable) VariableName() string { + if ignoreNameOverride(cv.Name.Name) { + return cv.Name.Name[1:] + } + + if overriddenName := nameOverride(cv.Comment); overriddenName != "" { + return overriddenName + } + + return cv.Name.Name +} + var escapedChars = map[uint8]uint8{ 'n': '\n', 'r': '\r', diff --git a/enums_test.go b/enums_test.go index d6645c7ce..2c2fd481d 100644 --- a/enums_test.go +++ b/enums_test.go @@ -18,9 +18,11 @@ func TestParseGlobalEnums(t *testing.T) { p := New() err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) + constsPath := "github.com/swaggo/swag/testdata/enums/consts" assert.Equal(t, bits.UintSize, p.packages.packages[constsPath].ConstTable["uintSize"].Value) assert.Equal(t, int32(62), p.packages.packages[constsPath].ConstTable["maxBase"].Value) @@ -31,4 +33,22 @@ func TestParseGlobalEnums(t *testing.T) { assert.Equal(t, "aa\nbb\u8888cc", p.packages.packages[constsPath].ConstTable["escapestr"].Value) assert.Equal(t, 1_000_000, p.packages.packages[constsPath].ConstTable["underscored"].Value) assert.Equal(t, 0b10001000, p.packages.packages[constsPath].ConstTable["binaryInteger"].Value) + + typesPath := "github.com/swaggo/swag/testdata/enums/types" + + difficultyEnums := p.packages.packages[typesPath].TypeDefinitions["Difficulty"].Enums + assert.Equal(t, "Easy", difficultyEnums[0].key) + assert.Equal(t, "", difficultyEnums[0].Comment) + assert.Equal(t, "Medium", difficultyEnums[1].key) + assert.Equal(t, "This one also has a comment", difficultyEnums[1].Comment) + assert.Equal(t, "DifficultyHard", difficultyEnums[2].key) + assert.Equal(t, "This means really hard", difficultyEnums[2].Comment) + + securityLevelEnums := p.packages.packages[typesPath].TypeDefinitions["SecurityClearance"].Enums + assert.Equal(t, "Public", securityLevelEnums[0].key) + assert.Equal(t, "", securityLevelEnums[0].Comment) + assert.Equal(t, "SecurityClearanceSensitive", securityLevelEnums[1].key) + assert.Equal(t, "Name override and comment rules apply here just as above", securityLevelEnums[1].Comment) + assert.Equal(t, "SuperSecret", securityLevelEnums[2].key) + assert.Equal(t, "This one has a name override and a comment", securityLevelEnums[2].Comment) } diff --git a/packages.go b/packages.go index f63b112ac..e03c0272d 100644 --- a/packages.go +++ b/packages.go @@ -391,21 +391,15 @@ func (pkgDefs *PackagesDefinitions) collectConstEnums(parsedSchemas map[*TypeSpe typeDef.Enums = make([]EnumValue, 0) } - name := constVar.Name.Name + name := constVar.VariableName() if _, ok = constVar.Value.(ast.Expr); ok { continue } enumValue := EnumValue{ - key: name, - Value: constVar.Value, - } - if constVar.Comment != nil && len(constVar.Comment.List) > 0 { - enumValue.Comment = constVar.Comment.List[0].Text - enumValue.Comment = strings.TrimPrefix(enumValue.Comment, "//") - enumValue.Comment = strings.TrimPrefix(enumValue.Comment, "/*") - enumValue.Comment = strings.TrimSuffix(enumValue.Comment, "*/") - enumValue.Comment = strings.TrimSpace(enumValue.Comment) + key: name, + Value: constVar.Value, + Comment: commentWithoutNameOverride(constVar.Comment), } typeDef.Enums = append(typeDef.Enums, enumValue) } diff --git a/parser.go b/parser.go index 3e12c2fc0..f52d0a70b 100644 --- a/parser.go +++ b/parser.go @@ -1354,9 +1354,9 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) for _, value := range typeSpecDef.Enums { definition.Enum = append(definition.Enum, value.Value) varnames = append(varnames, value.key) + enumDescriptions = append(enumDescriptions, value.Comment) if len(value.Comment) > 0 { enumComments[value.key] = value.Comment - enumDescriptions = append(enumDescriptions, value.Comment) } } if definition.Extensions == nil { diff --git a/schema.go b/schema.go index 234eb3fc5..64474eb70 100644 --- a/schema.go +++ b/schema.go @@ -4,6 +4,9 @@ import ( "errors" "fmt" "github.com/go-openapi/spec" + "go/ast" + "regexp" + "strings" ) const ( @@ -172,6 +175,44 @@ func ignoreNameOverride(name string) bool { return len(name) != 0 && name[0] == IgnoreNameOverridePrefix } +var overrideNameRegex = regexp.MustCompile(`(?i)^@name\s+(\S+)`) + +func nameOverride(commentGroup *ast.CommentGroup) string { + if commentGroup == nil { + return "" + } + + // get alias from comment '// @name ' + for _, comment := range commentGroup.List { + trimmedComment := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) + texts := overrideNameRegex.FindStringSubmatch(trimmedComment) + if len(texts) > 1 { + return texts[1] + } + } + + return "" +} + +func commentWithoutNameOverride(commentGroup *ast.CommentGroup) string { + if commentGroup == nil { + return "" + } + + commentBuilder := strings.Builder{} + for _, comment := range commentGroup.List { + commentText := comment.Text + commentText = strings.TrimPrefix(commentText, "//") + commentText = strings.TrimPrefix(commentText, "/*") + commentText = strings.TrimSuffix(commentText, "*/") + commentText = strings.TrimSpace(commentText) + commentText = overrideNameRegex.ReplaceAllString(commentText, "") + commentText = strings.TrimSpace(commentText) + commentBuilder.WriteString(commentText) + } + return commentBuilder.String() +} + // IsComplexSchema whether a schema is complex and should be a ref schema func IsComplexSchema(schema *spec.Schema) bool { // a enum type should be complex diff --git a/testdata/enums/expected.json b/testdata/enums/expected.json index 5235f0ae9..334a4fc38 100644 --- a/testdata/enums/expected.json +++ b/testdata/enums/expected.json @@ -103,8 +103,12 @@ "B": "BBB" }, "x-enum-descriptions": [ + "", "AAA", - "BBB" + "BBB", + "", + "", + "" ], "x-enum-varnames": [ "None", @@ -117,6 +121,30 @@ "name": "class", "in": "formData" }, + { + "enum": [ + "easy", + "medium", + "hard" + ], + "type": "string", + "x-enum-comments": { + "DifficultyHard": "This means really hard", + "Medium": "This one also has a comment" + }, + "x-enum-descriptions": [ + "", + "This one also has a comment", + "This means really hard" + ], + "x-enum-varnames": [ + "Easy", + "Medium", + "DifficultyHard" + ], + "name": "difficulty", + "in": "formData" + }, { "enum": [ 1, @@ -136,7 +164,8 @@ "Mask1", "Mask2", "Mask3", - "Mask4" + "Mask4", + "" ], "x-enum-varnames": [ "Mask1", @@ -153,6 +182,30 @@ "name": "name", "in": "formData" }, + { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "x-enum-comments": { + "SecurityClearanceSensitive": "Name override and comment rules apply here just as above", + "SuperSecret": "This one has a name override and a comment" + }, + "x-enum-descriptions": [ + "", + "Name override and comment rules apply here just as above", + "This one has a name override and a comment" + ], + "x-enum-varnames": [ + "Public", + "SecurityClearanceSensitive", + "SuperSecret" + ], + "name": "securityClearance", + "in": "formData" + }, { "enum": [ 77, @@ -221,6 +274,19 @@ "name": "class", "in": "formData" }, + { + "type": "array", + "items": { + "enum": [ + "easy", + "medium", + "hard" + ], + "type": "string" + }, + "name": "difficulty", + "in": "formData" + }, { "type": "array", "items": { @@ -241,6 +307,19 @@ "name": "name", "in": "formData" }, + { + "type": "array", + "items": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer" + }, + "name": "securityClearance", + "in": "formData" + }, { "enum": [ "teacher", @@ -291,8 +370,12 @@ "B": "BBB" }, "x-enum-descriptions": [ + "", "AAA", - "BBB" + "BBB", + "", + "", + "" ], "x-enum-varnames": [ "None", @@ -303,6 +386,28 @@ "F" ] }, + "types.Difficulty": { + "type": "string", + "enum": [ + "easy", + "medium", + "hard" + ], + "x-enum-comments": { + "DifficultyHard": "This means really hard", + "Medium": "This one also has a comment" + }, + "x-enum-descriptions": [ + "", + "This one also has a comment", + "This means really hard" + ], + "x-enum-varnames": [ + "Easy", + "Medium", + "DifficultyHard" + ] + }, "types.Mask": { "type": "integer", "enum": [ @@ -322,7 +427,8 @@ "Mask1", "Mask2", "Mask3", - "Mask4" + "Mask4", + "" ], "x-enum-varnames": [ "Mask1", @@ -338,12 +444,18 @@ "class": { "$ref": "#/definitions/types.Class" }, + "difficulty": { + "$ref": "#/definitions/types.Difficulty" + }, "mask": { "$ref": "#/definitions/types.Mask" }, "name": { "type": "string" }, + "securityClearance": { + "$ref": "#/definitions/types.SecurityClearance" + }, "sex": { "$ref": "#/definitions/types.Sex" }, @@ -352,6 +464,28 @@ } } }, + "types.SecurityClearance": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-comments": { + "SecurityClearanceSensitive": "Name override and comment rules apply here just as above", + "SuperSecret": "This one has a name override and a comment" + }, + "x-enum-descriptions": [ + "", + "Name override and comment rules apply here just as above", + "This one has a name override and a comment" + ], + "x-enum-varnames": [ + "Public", + "SecurityClearanceSensitive", + "SuperSecret" + ] + }, "types.Sex": { "type": "integer", "format": "int32", diff --git a/testdata/enums/types/model.go b/testdata/enums/types/model.go index 1b730d5a8..7d0fc8493 100644 --- a/testdata/enums/types/model.go +++ b/testdata/enums/types/model.go @@ -50,17 +50,37 @@ const ( Female Sex = 'F' ) +type Difficulty string + +const ( + DifficultyEasy Difficulty = "easy" // @name Easy + DifficultyMedium Difficulty = "medium" // @Name Medium This one also has a comment + DifficultyHard Difficulty = "hard" // This means really hard +) + +type SecurityClearance int + +const ( + SecurityClearancePublic SecurityClearance = iota // @name Public + SecurityClearanceSensitive // Name override and comment rules apply here just as above + SecurityClearanceSecret // @name SuperSecret This one has a name override and a comment +) + type Person struct { - Name string - Class Class - Mask Mask - Type Type - Sex Sex + Name string + Class Class + Mask Mask + Type Type + Sex Sex + Difficulty Difficulty + SecurityClearance SecurityClearance } type PersonWithArrayEnum struct { - Name string - Class []Class - Mask []Mask - Type Type + Name string + Class []Class + Mask []Mask + Difficulty []Difficulty + SecurityClearance []SecurityClearance + Type Type } diff --git a/types.go b/types.go index 5f3031e0b..d7fe3222f 100644 --- a/types.go +++ b/types.go @@ -3,7 +3,6 @@ package swag import ( "go/ast" "go/token" - "regexp" "strings" "github.com/go-openapi/spec" @@ -74,25 +73,8 @@ func (t *TypeSpecDef) FullPath() string { return t.PkgPath + "." + t.Name() } -const regexCaseInsensitive = "(?i)" - -var reTypeName = regexp.MustCompile(regexCaseInsensitive + `^@name\s+(\S+)`) - func (t *TypeSpecDef) Alias() string { - if t.TypeSpec.Comment == nil { - return "" - } - - // get alias from comment '// @name ' - for _, comment := range t.TypeSpec.Comment.List { - trimmedComment := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) - texts := reTypeName.FindStringSubmatch(trimmedComment) - if len(texts) > 1 { - return texts[1] - } - } - - return "" + return nameOverride(t.TypeSpec.Comment) } func (t *TypeSpecDef) SetSchemaName() { From 252fecd4cbc33868a56d3f3f1092cd2a561073ef Mon Sep 17 00:00:00 2001 From: Stephan Kast Date: Fri, 25 Jul 2025 03:30:41 +0200 Subject: [PATCH 4/5] Use the structs name without the @name comment (#2043) * Fixed struct naming when using dependency parsing * Fixed naming * Fixed debug message always triggering --------- Co-authored-by: skast --- cmd/swag/main.go | 7 +++++++ gen/gen.go | 4 ++++ parser.go | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/cmd/swag/main.go b/cmd/swag/main.go index 0a3cacc52..6c0b67e7c 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -24,6 +24,7 @@ const ( outputTypesFlag = "outputTypes" parseVendorFlag = "parseVendor" parseDependencyFlag = "parseDependency" + useStructNameFlag = "useStructName" parseDependencyLevelFlag = "parseDependencyLevel" markdownFilesFlag = "markdownFiles" codeExampleFilesFlag = "codeExampleFiles" @@ -99,6 +100,11 @@ var initFlags = []cli.Flag{ Aliases: []string{"pd"}, Usage: "Parse go files inside dependency folder, disabled by default", }, + &cli.BoolFlag{ + Name: useStructNameFlag, + Aliases: []string{"st"}, + Usage: "Dont use those ugly full-path names when using dependency flag", + }, &cli.StringFlag{ Name: markdownFilesFlag, Aliases: []string{"md"}, @@ -251,6 +257,7 @@ func initAction(ctx *cli.Context) error { ParseDependency: pdv, MarkdownFilesDir: ctx.String(markdownFilesFlag), ParseInternal: ctx.Bool(parseInternalFlag), + UseStructNames: ctx.Bool(useStructNameFlag), GeneratedTime: ctx.Bool(generatedTimeFlag), RequiredByDefault: ctx.Bool(requiredByDefaultFlag), CodeExampleFilesDir: ctx.String(codeExampleFilesFlag), diff --git a/gen/gen.go b/gen/gen.go index 417d51c36..1180adb68 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -108,6 +108,9 @@ type Config struct { // ParseDependencies whether swag should be parse outside dependency folder: 0 none, 1 models, 2 operations, 3 all ParseDependency int + // UseStructNames stick to the struct name instead of those ugly full-path names + UseStructNames bool + // ParseInternal whether swag should parse internal packages ParseInternal bool @@ -198,6 +201,7 @@ func (g *Gen) Build(config *Config) error { p := swag.New( swag.SetParseDependency(config.ParseDependency), + swag.SetUseStructName(config.UseStructNames), swag.SetMarkdownFileDirectory(config.MarkdownFilesDir), swag.SetDebugger(config.Debugger), swag.SetExcludedDirsAndFiles(config.Excludes), diff --git a/parser.go b/parser.go index f52d0a70b..cd0142bb0 100644 --- a/parser.go +++ b/parser.go @@ -182,6 +182,9 @@ type Parser struct { // ParseFuncBody whether swag should parse api info inside of funcs ParseFuncBody bool + + // UseStructName Dont use those ugly full-path names when using dependency flag + UseStructName bool } // FieldParserFactory create FieldParser. @@ -261,6 +264,13 @@ func SetParseDependency(parseDependency int) func(*Parser) { } } +// SetUseStructName sets whether to strip the full-path definition name. +func SetUseStructName(useStructName bool) func(*Parser) { + return func(p *Parser) { + p.UseStructName = useStructName + } +} + // SetMarkdownFileDirectory sets the directory to search for markdown files. func SetMarkdownFileDirectory(directoryPath string) func(*Parser) { return func(p *Parser) { @@ -1330,6 +1340,16 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) ErrRecursiveParseStruct } + if parser.UseStructName { + schemaName := strings.Split(typeSpecDef.SchemaName, ".") + if len(schemaName) > 1 { + typeSpecDef.SchemaName = schemaName[len(schemaName)-1] + typeName = typeSpecDef.SchemaName + } else { + parser.debug.Printf("Could not strip type name of %s", typeName) + } + } + parser.structStack = append(parser.structStack, typeSpecDef) parser.debug.Printf("Generating %s", typeName) From f676981e12b892b4a2c5f50cd3dbc96d469ca358 Mon Sep 17 00:00:00 2001 From: Berk Karaal Date: Mon, 28 Jul 2025 06:01:37 +0300 Subject: [PATCH 5/5] feat: allow description line continuation (#2048) --- operation.go | 2 +- parser.go | 3 +- parser_test.go | 16 +++++++ .../description_line_continuation/api/api.go | 33 +++++++++++++++ .../expected.json | 34 +++++++++++++++ .../description_line_continuation/main.go | 25 +++++++++++ utils.go | 14 ++++++- utils_test.go | 42 ++++++++++++++++++- 8 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 testdata/description_line_continuation/api/api.go create mode 100644 testdata/description_line_continuation/expected.json create mode 100644 testdata/description_line_continuation/main.go diff --git a/operation.go b/operation.go index 185f823bc..f3b776141 100644 --- a/operation.go +++ b/operation.go @@ -204,7 +204,7 @@ func (operation *Operation) ParseDescriptionComment(lineRemainder string) { return } - operation.Description += "\n" + lineRemainder + operation.Description = AppendDescription(operation.Description, lineRemainder) } // ParseMetadata parse metadata. diff --git a/parser.go b/parser.go index cd0142bb0..6d550cb01 100644 --- a/parser.go +++ b/parser.go @@ -546,8 +546,7 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { setSwaggerInfo(parser.swagger, attr, value) case descriptionAttr: if previousAttribute == attribute { - parser.swagger.Info.Description += "\n" + value - + parser.swagger.Info.Description = AppendDescription(parser.swagger.Info.Description, value) continue } diff --git a/parser_test.go b/parser_test.go index 45ef34483..b37c9ffd4 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4491,3 +4491,19 @@ var Func2 = Func assert.NotNil(t, val2.Get) assert.Equal(t, val2.Get.OperationProps.Summary, "generate indirectly pointing") } + +func TestParser_DescriptionLineContinuation(t *testing.T) { + t.Parallel() + + p := New() + searchDir := "testdata/description_line_continuation" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} diff --git a/testdata/description_line_continuation/api/api.go b/testdata/description_line_continuation/api/api.go new file mode 100644 index 000000000..500db765c --- /dev/null +++ b/testdata/description_line_continuation/api/api.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" +) + +// @Summary Endpoint A +// @Description This is a mock endpoint description \ +// @Description which is long and descriptions that \ +// @Description end with backslash do not add a new line. +// @Description This sentence is in a new line. +// @Description +// @Description And this have an empty line above it. +// @Description Lorem ipsum dolor sit amet \ +// @Description consectetur adipiscing elit, \ +// @Description sed do eiusmod tempor incididunt \ +// @Description ut labore et dolore magna aliqua. +// @Success 200 +// @Router /a [get] +func EndpointA(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +// @Summary Endpoint B +// @Description Something something. +// @Description +// @Description A new line, \ +// @Description continue to the line. +// @Success 200 +// @Router /b [get] +func EndpointB(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/testdata/description_line_continuation/expected.json b/testdata/description_line_continuation/expected.json new file mode 100644 index 000000000..c814fcb5c --- /dev/null +++ b/testdata/description_line_continuation/expected.json @@ -0,0 +1,34 @@ +{ + "swagger": "2.0", + "info": { + "description": "Example long description that should not be split into multiple lines.\nThis is a new line thatescapes new line withoutadding a whitespace.\n\nAnother line that has an empty line above it.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:8080", + "paths": { + "/a": { + "get": { + "description": "This is a mock endpoint description which is long and descriptions that end with backslash do not add a new line.\nThis sentence is in a new line.\n\nAnd this have an empty line above it.\nLorem ipsum dolor sit amet consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "summary": "Endpoint A", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/b": { + "get": { + "description": "Something something.\n\nA new line, continue to the line.", + "summary": "Endpoint B", + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} \ No newline at end of file diff --git a/testdata/description_line_continuation/main.go b/testdata/description_line_continuation/main.go new file mode 100644 index 000000000..33f2714d3 --- /dev/null +++ b/testdata/description_line_continuation/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/description_escape_new_line/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description Example long description \ +// @description that should not be split \ +// @description into multiple lines. +// @description This is a new line that\ +// @description escapes new line without\ +// @description adding a whitespace. +// @description +// @description Another line that has an \ +// @description empty line above it. +// @host localhost:8080 +func main() { + http.HandleFunc("/a", api.EndpointA) + http.HandleFunc("/b", api.EndpointB) + http.ListenAndServe(":8080", nil) +} diff --git a/utils.go b/utils.go index df31ff2e1..6edf54f2c 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,9 @@ package swag -import "unicode" +import ( + "strings" + "unicode" +) // FieldsFunc split a string s by a func splitter into max n parts func FieldsFunc(s string, f func(rune2 rune) bool, n int) []string { @@ -53,3 +56,12 @@ func FieldsFunc(s string, f func(rune2 rune) bool, n int) []string { func FieldsByAnySpace(s string, n int) []string { return FieldsFunc(s, unicode.IsSpace, n) } + +// AppendDescription appends a new string to the existing description, treating +// a trailing backslash as a line continuation. +func AppendDescription(current, addition string) string { + if strings.HasSuffix(current, "\\") { + return current[:len(current)-1] + addition + } + return current + "\n" + addition +} diff --git a/utils_test.go b/utils_test.go index 1c4d9953a..25395be4f 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,8 +1,9 @@ package swag import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestFieldsByAnySpace(t *testing.T) { @@ -36,3 +37,42 @@ func TestFieldsByAnySpace(t *testing.T) { }) } } + +func TestAppendDescription(t *testing.T) { + type args struct { + current string + addition string + } + tests := []struct { + name string + args args + want string + }{ + {"test1", + args{ + "aa", + "bb", + }, + "aa\nbb", + }, + {"test2", + args{ + "aa\\", + "bb", + }, + "aabb", + }, + {"test3", + args{ + "aa \\", + "bb", + }, + "aa bb", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, AppendDescription(tt.args.current, tt.args.addition), "AppendDescription(%v, %v)", tt.args.current, tt.args.addition) + }) + } +}