From 2db11dd1844f45698364695f600012a741bfa5f0 Mon Sep 17 00:00:00 2001 From: Shane O'Donovan Date: Tue, 5 May 2026 13:48:51 +0100 Subject: [PATCH 1/2] Fix package name collision when two packages share the same base name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When two packages have the same last path component (e.g. sub-a/shared and sub-b/shared both resolve to "shared"), GetReference was using filepath.Base(pkg) as the lookup key, causing the second type to silently overwrite the first in prog.References. The fix adds a collision check before storing: if the key already exists for a different package, a numeric suffix is appended (shared.Request2, shared.Request3, …) to produce a unique key. Repeated calls for the same colliding package reuse the existing suffixed key. Co-Authored-By: Claude Sonnet 4.6 --- docparse/find.go | 14 ++++ .../openapi2/src/package-name-collision/in.go | 16 +++++ .../package-name-collision/sub-a/shared/in.go | 5 ++ .../package-name-collision/sub-b/shared/in.go | 5 ++ .../src/package-name-collision/want.yaml | 64 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 testdata/openapi2/src/package-name-collision/in.go create mode 100644 testdata/openapi2/src/package-name-collision/sub-a/shared/in.go create mode 100644 testdata/openapi2/src/package-name-collision/sub-b/shared/in.go create mode 100644 testdata/openapi2/src/package-name-collision/want.yaml diff --git a/docparse/find.go b/docparse/find.go index b951b0c..b3714aa 100644 --- a/docparse/find.go +++ b/docparse/find.go @@ -473,6 +473,20 @@ func GetReference(prog *Program, context string, isEmbed bool, lookup, filePath } } + // Ensure the lookup key is unique when two packages share the same base name + // (e.g. task/reminder and time/reminder both produce "reminder.Request"). + if existing, ok := prog.References[ref.Lookup]; ok && existing.Package != ref.Package { + for i := 2; ; i++ { + candidate := fmt.Sprintf("%s%d", ref.Lookup, i) + if ex, ok := prog.References[candidate]; !ok { + ref.Lookup = candidate + break + } else if ex.Package == ref.Package { + ref.Lookup = candidate + break + } + } + } prog.References[ref.Lookup] = ref var ( nested []string diff --git a/testdata/openapi2/src/package-name-collision/in.go b/testdata/openapi2/src/package-name-collision/in.go new file mode 100644 index 0000000..679cd30 --- /dev/null +++ b/testdata/openapi2/src/package-name-collision/in.go @@ -0,0 +1,16 @@ +package package_name_collision + +import ( + sharedA "package-name-collision/sub-a/shared" + sharedB "package-name-collision/sub-b/shared" +) + +// POST /foo create foo +// +// Request body: sharedA.Request +// Response 200: {empty} + +// POST /bar create bar +// +// Request body: sharedB.Request +// Response 200: {empty} diff --git a/testdata/openapi2/src/package-name-collision/sub-a/shared/in.go b/testdata/openapi2/src/package-name-collision/sub-a/shared/in.go new file mode 100644 index 0000000..3ac34b2 --- /dev/null +++ b/testdata/openapi2/src/package-name-collision/sub-a/shared/in.go @@ -0,0 +1,5 @@ +package shared + +type Request struct { + ID int64 `json:"id"` +} diff --git a/testdata/openapi2/src/package-name-collision/sub-b/shared/in.go b/testdata/openapi2/src/package-name-collision/sub-b/shared/in.go new file mode 100644 index 0000000..673ef8f --- /dev/null +++ b/testdata/openapi2/src/package-name-collision/sub-b/shared/in.go @@ -0,0 +1,5 @@ +package shared + +type Request struct { + Name string `json:"name"` +} diff --git a/testdata/openapi2/src/package-name-collision/want.yaml b/testdata/openapi2/src/package-name-collision/want.yaml new file mode 100644 index 0000000..11b07b6 --- /dev/null +++ b/testdata/openapi2/src/package-name-collision/want.yaml @@ -0,0 +1,64 @@ +swagger: "2.0" +info: + title: x + version: x +consumes: + - application/json +produces: + - application/json +tags: + - name: bar + - name: create + - name: foo +paths: + /bar: + post: + operationId: POST_bar + tags: + - create + - bar + consumes: + - application/json + produces: + - application/json + parameters: + - name: shared.Request2 + in: body + required: true + schema: + $ref: '#/definitions/shared.Request2' + responses: + 200: + description: 200 OK (no data) + /foo: + post: + operationId: POST_foo + tags: + - create + - foo + consumes: + - application/json + produces: + - application/json + parameters: + - name: shared.Request + in: body + required: true + schema: + $ref: '#/definitions/shared.Request' + responses: + 200: + description: 200 OK (no data) +definitions: + shared.Request: + title: Request + type: object + properties: + id: + type: integer + shared.Request2: + title: Request + type: object + properties: + name: + type: string From 64c2e5c53c578d0eecd454857098bfb62ff78b42 Mon Sep 17 00:00:00 2001 From: Shane O'Donovan Date: Tue, 5 May 2026 13:57:55 +0100 Subject: [PATCH 2/2] Extract applyFieldWhitelists to reduce GetReference cyclomatic complexity GetReference exceeded the gocyclo threshold of 65 (was 68). Extracting the field-whitelist processing block into its own helper brings it back under the limit without changing behaviour. Co-Authored-By: Claude Sonnet 4.6 --- docparse/find.go | 102 ++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 55 deletions(-) diff --git a/docparse/find.go b/docparse/find.go index b3714aa..913a595 100644 --- a/docparse/find.go +++ b/docparse/find.go @@ -555,42 +555,73 @@ func GetReference(prog *Program, context string, isEmbed bool, lookup, filePath } ref.Schema = schema - changed := false + if err := applyFieldWhitelists(prog, context, filePath, name, tagName, &ref); err != nil { + return nil, err + } + + // Merge for embedded structs without a tag. + for _, n := range nested { + ref.Fields = append(ref.Fields, prog.References[n].Fields...) + + if prog.References[n].Schema != nil { + for k, v := range prog.References[n].Schema.Properties { + if _, ok := ref.Schema.Properties[k]; !ok { + ref.Schema.Properties[k] = v + } + } + } + } + + if ref.IsSlice { + sliceSchema := &Schema{ + Type: "array", + Items: ref.Schema, + } + ref.Schema = sliceSchema + } + if ref.Wrapper != "" { + wrappedSchema := &Schema{ + Title: ref.Name, + Type: "object", + Properties: map[string]*Schema{}, + } + + wrappedSchema.Properties[ref.Wrapper] = ref.Schema + ref.Schema = wrappedSchema + } + + prog.References[ref.Lookup] = ref + + return &ref, nil +} + +func applyFieldWhitelists(prog *Program, context, filePath, name, tagName string, ref *Reference) error { + changed := false for _, p := range ref.Schema.Properties { - // Check if any fields are whitelisted, if not continue onto next property if len(p.FieldWhitelist) == 0 { continue } - changed = true - - // Get the package so we can lookup the correct reference split := strings.Split(p.Reference, ".") lookupStruct := strings.Join(split[:len(split)-1], ".") if lookupStruct != "" { lookupStruct += "." } - for i, f := range ref.Fields { if lookupStruct+f.Name != p.Reference { continue } - - // Find the referenced struct reference, err := GetReference(prog, context, false, lookupStruct+f.Name, filePath) if err != nil { - return nil, fmt.Errorf("could not get referenced struct %s", lookupStruct+f.Name) + return fmt.Errorf("could not get referenced struct %s", lookupStruct+f.Name) } - fields := []*ast.Field{} for _, field := range reference.Fields { if sliceutil.Contains(p.FieldWhitelist, strings.ToLower(field.Name)) { fields = append(fields, field.KindField) } } - - // Construct the parameter using the given fields ref.Fields[i] = Param{ Name: f.Name, KindField: &ast.Field{ @@ -599,10 +630,8 @@ func GetReference(prog *Program, context string, isEmbed bool, lookup, filePath }, Names: f.KindField.Names, Type: &ast.StructType{ - Struct: 0, Fields: &ast.FieldList{ - Opening: 0, - List: fields, + List: fields, }, }, Tag: f.KindField.Tag, @@ -611,51 +640,14 @@ func GetReference(prog *Program, context string, isEmbed bool, lookup, filePath } } } - - // If the fields have been changed, regenerate the schema with the new fields if changed { - schema, err = structToSchema(prog, name, tagName, ref) + schema, err := structToSchema(prog, name, tagName, *ref) if err != nil { - return nil, fmt.Errorf("%v can not be converted to JSON schema: %v", name, err) + return fmt.Errorf("%v can not be converted to JSON schema: %v", name, err) } ref.Schema = schema } - - // Merge for embedded structs without a tag. - for _, n := range nested { - ref.Fields = append(ref.Fields, prog.References[n].Fields...) - - if prog.References[n].Schema != nil { - for k, v := range prog.References[n].Schema.Properties { - if _, ok := ref.Schema.Properties[k]; !ok { - ref.Schema.Properties[k] = v - } - } - } - } - - if ref.IsSlice { - sliceSchema := &Schema{ - Type: "array", - Items: ref.Schema, - } - ref.Schema = sliceSchema - } - - if ref.Wrapper != "" { - wrappedSchema := &Schema{ - Title: ref.Name, - Type: "object", - Properties: map[string]*Schema{}, - } - - wrappedSchema.Properties[ref.Wrapper] = ref.Schema - ref.Schema = wrappedSchema - } - - prog.References[ref.Lookup] = ref - - return &ref, nil + return nil } func findNested(prog *Program, context string, isEmbed bool, f *ast.Field, filePath, pkg string) (string, error) {