Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: CI
on:
push: {}
pull_request: {}
workflow_dispatch: {}
permissions:
contents: read
jobs:
build:
uses: oapi-codegen/actions/.github/workflows/ci.yml@6cf35d4f044f2663dae54547ff6d426e565beb48 # v0.6.0
with:
excluding_versions: '["1.24"]'
lint_versions: '["1.25"]'
7 changes: 6 additions & 1 deletion codegen/internal/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package codegen

import (
"fmt"
"sort"
"strings"

"github.com/pb33f/libopenapi"
Expand Down Expand Up @@ -1041,12 +1042,16 @@ func collectUnionMembers(gen *TypeGenerator, parentDesc *SchemaDescriptor, membe
}
}

// Build reverse mapping: $ref path → []discriminator values
// Build reverse mapping: $ref path → []discriminator values.
// Sort values so output is deterministic regardless of map iteration order.
refToDiscValues := make(map[string][]string)
if parentDesc != nil && parentDesc.Discriminator != nil {
for discValue, refPath := range parentDesc.Discriminator.Mapping {
refToDiscValues[refPath] = append(refToDiscValues[refPath], discValue)
}
for ref := range refToDiscValues {
sort.Strings(refToDiscValues[ref])
}
}

for i, proxy := range memberProxies {
Expand Down
56 changes: 51 additions & 5 deletions codegen/internal/dce/dce.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,53 @@ func EliminateDeadCode(src string) (string, error) {
}
}

// Keep only reachable declarations.
var kept []ast.Decl
for _, d := range roots {
kept = append(kept, d)
}
// Keep only reachable declarations; collect positions of eliminated ones.
kept := append([]ast.Decl{}, roots...)
var eliminated []ast.Decl
for _, c := range candidates {
if isReachable(c.names, reachable) {
kept = append(kept, c.decl)
} else {
eliminated = append(eliminated, c.decl)
}
}
f.Decls = kept

// Remove comments associated with eliminated declarations so
// go/printer doesn't emit orphaned doc/inline comments.
if len(eliminated) > 0 {
// Build line ranges for eliminated declarations: from the doc
// comment (or decl start) through the last line of the decl.
removedLines := make([]lineSpan, 0, len(eliminated))
for _, d := range eliminated {
start := d.Pos()
switch dd := d.(type) {
case *ast.GenDecl:
if dd.Doc != nil {
start = dd.Doc.Pos()
}
case *ast.FuncDecl:
if dd.Doc != nil {
start = dd.Doc.Pos()
}
}
removedLines = append(removedLines, lineSpan{
startLine: fset.Position(start).Line,
endLine: fset.Position(d.End()).Line,
})
}

filtered := make([]*ast.CommentGroup, 0, len(f.Comments))
for _, cg := range f.Comments {
cgStart := fset.Position(cg.Pos()).Line
cgEnd := fset.Position(cg.End()).Line
if !linesOverlap(cgStart, cgEnd, removedLines) {
filtered = append(filtered, cg)
}
}
f.Comments = filtered
}

var buf strings.Builder
if err := printer.Fprint(&buf, fset, f); err != nil {
return "", err
Expand Down Expand Up @@ -154,6 +189,17 @@ func receiverTypeName(expr ast.Expr) string {
return ""
}

type lineSpan struct{ startLine, endLine int }

func linesOverlap(cgStart, cgEnd int, spans []lineSpan) bool {
for _, s := range spans {
if cgStart >= s.startLine && cgEnd <= s.endLine {
return true
}
}
return false
}

func collectIdents(node ast.Node, idents map[string]bool) {
ast.Inspect(node, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok {
Expand Down
186 changes: 186 additions & 0 deletions codegen/internal/dce/dce_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package dce

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEliminateDeadCode_RemovesOrphanedComments(t *testing.T) {
src := `package gen

func usedFunc() {
helperFunc()
}

// --- oapi-runtime begin ---

// helperFunc is used by usedFunc.
func helperFunc() {}

// unusedFunc does nothing useful.
// It has a multi-line doc comment.
func unusedFunc() {}

// --- oapi-runtime end ---
`

result, err := EliminateDeadCode(src)
require.NoError(t, err)

assert.Contains(t, result, "helperFunc")
assert.NotContains(t, result, "unusedFunc")
}

func TestEliminateDeadCode_NoMarkers(t *testing.T) {
src := `package gen

func foo() {}
`
result, err := EliminateDeadCode(src)
require.NoError(t, err)
assert.Equal(t, src, result)
}

func TestEliminateDeadCode_AllUsed(t *testing.T) {
src := `package gen

func caller() {
a()
b()
}

// --- oapi-runtime begin ---

// a does something.
func a() {}

// b does something else.
func b() {}

// --- oapi-runtime end ---
`

result, err := EliminateDeadCode(src)
require.NoError(t, err)

assert.Contains(t, result, "func a()")
assert.Contains(t, result, "func b()")
assert.Contains(t, result, "a does something")
assert.Contains(t, result, "b does something else")
}

func TestEliminateDeadCode_NoneUsed(t *testing.T) {
src := `package gen

func caller() {}

// --- oapi-runtime begin ---

// orphanA is unused.
func orphanA() {}

// orphanB is also unused.
func orphanB() {}

// --- oapi-runtime end ---
`

result, err := EliminateDeadCode(src)
require.NoError(t, err)

assert.NotContains(t, result, "orphanA")
assert.NotContains(t, result, "orphanB")
}

func TestEliminateDeadCode_InlineComments(t *testing.T) {
src := `package gen

func caller() {
used()
}

// --- oapi-runtime begin ---

func used() {} // keep this

func unused() {} // drop this

// --- oapi-runtime end ---
`

result, err := EliminateDeadCode(src)
require.NoError(t, err)

assert.Contains(t, result, "keep this")
assert.NotContains(t, result, "drop this")
}

func TestEliminateDeadCode_TypeDecl(t *testing.T) {
src := `package gen

// UsedType is referenced.
type UsedType struct{}

// --- oapi-runtime begin ---

// helperType is used by UsedType (transitively).
type helperType = UsedType

// unusedType is not referenced.
type unusedType struct {
field string
}

// --- oapi-runtime end ---
`

result, err := EliminateDeadCode(src)
require.NoError(t, err)

// helperType is reachable because it references UsedType which is in roots,
// and helperType itself may or may not be reachable depending on the algorithm.
// The key assertion: unusedType and its comment should be gone.
assert.NotContains(t, result, "unusedType")
assert.NotContains(t, result, "unusedType is not referenced")
}

func TestEliminateDeadCode_PreservesNonRuntimeComments(t *testing.T) {
src := `package gen

// Package-level comment that should survive.

// caller calls helper.
func caller() {
helper()
}

// --- oapi-runtime begin ---

// helper is needed.
func helper() {}

// unused is not needed.
func unused() {}

// --- oapi-runtime end ---
`

result, err := EliminateDeadCode(src)
require.NoError(t, err)

assert.Contains(t, result, "Package-level comment")
assert.Contains(t, result, "caller calls helper")
assert.Contains(t, result, "helper is needed")
assert.NotContains(t, result, "unused is not needed")
// The word "unused" should only not appear as a function or its comment
lines := strings.Split(result, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.Contains(trimmed, "unused") && !strings.Contains(trimmed, "nolint") {
t.Errorf("unexpected reference to 'unused' in output: %s", trimmed)
}
}
}
31 changes: 0 additions & 31 deletions codegen/internal/runtime/params/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,34 +438,3 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) {
}
return b, nil
}

// structToFieldDict converts a struct to a map of field names to string values.
func structToFieldDict(value any) (map[string]string, error) {
v := reflect.ValueOf(value)
t := reflect.TypeOf(value)
fieldDict := make(map[string]string)

for i := 0; i < t.NumField(); i++ {
fieldT := t.Field(i)
tag := fieldT.Tag.Get("json")
fieldName := fieldT.Name
if tag != "" {
tagParts := strings.Split(tag, ",")
if tagParts[0] != "" {
fieldName = tagParts[0]
}
}
f := v.Field(i)

// Skip nil optional fields
if f.Type().Kind() == reflect.Ptr && f.IsNil() {
continue
}
str, err := primitiveToString(f.Interface())
if err != nil {
return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err)
}
fieldDict[fieldName] = str
}
return fieldDict, nil
}
4 changes: 2 additions & 2 deletions codegen/internal/test/callbacks/output/callbacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ func TestSchemaInstantiation(t *testing.T) {

// Verify callback request body type alias
func TestCallbackRequestBodyAlias(t *testing.T) {
var body TreePlantedJSONRequestBody = TreePlantingResult{
var body = TreePlantedJSONRequestBody(TreePlantingResult{
Success: true,
}
})
assert.True(t, body.Success)
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading