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
10 changes: 5 additions & 5 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ to builders without direct coupling.
| File | Contents |
|------|----------|
| `api.go` | `Run(*Options) (*spec.Swagger, error)` entry point; re-exports `Options = scanner.Options` |
| `diagnostics.go` | Re-exports the diagnostic surface for `Options.OnDiagnostic` callers: `Diagnostic`/`Severity`/`Code` aliases + `Severity*` constants |
| `doc.go` | Package godoc |
| `errors.go` | `ErrCodeScan` sentinel error |

Expand Down Expand Up @@ -82,10 +83,6 @@ Each sub-package owns one concern; `walker.go` carries the per-block grammar dis
`SwaggerTypable`, `ValidationBuilder`, `OperationValidationBuilder`, `ValueParser`, `Objecter` —
the glue that lets `parsers` write into any builder's target without importing concrete builders.

### `internal/logger/` — debug logging

`debug.go` — gated on `Options.Debug`.

### `internal/scantest/` — test utilities (do **not** import from production code)

| File | Contents |
Expand Down Expand Up @@ -127,7 +124,10 @@ malformed input, the petstore, aliased schemas, go123-specific forms, and cross-
siblings), each with a diagnostic. For consumers (e.g. go-swagger) wanting bare refs.
- `SetXNullableForPointers` — emit `x-nullable: true` on pointer fields.
- `SkipExtensions` — suppress `x-go-*` vendor extensions.
- `Debug` — verbose logging via `internal/logger`.
- `OnDiagnostic` — callback sink for all scan-time observations (the only output
channel; codescan never writes to stdout/stderr).
- `Debug` — deprecated no-op (the legacy stderr debug logger was retired; wire
`OnDiagnostic` instead).

## Dependencies

Expand Down
1 change: 0 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ Internal tree:
| `internal/builders/validations` | Type-aware coercion (`CoerceEnum`, `ParseDefault`) + shape checks (`IsLegalForType`) |
| `internal/builders/resolvers` | `SwaggerSchemaForType`, identity / assertion helpers, items-chain ifaces adapters |
| `internal/ifaces` | `SwaggerTypable`, `ValidationBuilder`, `OperationValidationBuilder`, `ValueParser`, `Objecter` — decouples parsers from builders |
| `internal/logger` | Debug logging (gated on `Options.Debug`) |
| `internal/scantest` | Test utilities: golden compare, fixture loading, mocks, classification helpers |
| `internal/integration` | Black-box integration tests against `fixtures/integration/golden/*.json` |

Expand Down
28 changes: 4 additions & 24 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
package codescan

import (
"flag"
"io"
"log"
"os"
"path/filepath"
"testing"
Expand All @@ -16,27 +13,10 @@ import (

// Public-API smoke suite. Fixture-heavy tests live in internal/integration.

var enableDebug bool //nolint:gochecknoglobals // test flag registered in init

func init() { //nolint:gochecknoinits // registers test flags before TestMain
flag.BoolVar(&enableDebug, "enable-debug", false, "enable debug output in tests")
}

func TestMain(m *testing.M) {
flag.Parse()

if !enableDebug {
log.SetOutput(io.Discard)
} else {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.SetOutput(os.Stderr)
}

os.Exit(m.Run())
}

func TestApplication_DebugLogging(t *testing.T) {
// Exercises the logger.DebugLogf code path with Debug: true.
func TestApplication_DeprecatedDebugOption(t *testing.T) {
// Options.Debug is a deprecated no-op (the legacy debug logger was
// retired in favour of diagnostics). Verify Run still accepts it without
// error and produces a spec.
_, err := Run(&Options{
Packages: []string{"./goparsing/petstore/..."},
WorkDir: "fixtures",
Expand Down
35 changes: 35 additions & 0 deletions diagnostics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package codescan

import "github.com/go-openapi/codescan/internal/parsers/grammar"

// Diagnostic is one observation the scanner makes about the source it
// processes — a parse/validation issue, a dropped construct, or an
// informational note. Every scan-time observation is delivered to
// [Options.OnDiagnostic]; codescan never writes to stdout/stderr.
//
// Fields: Pos (a go/token.Position, zero when no single source location
// applies), Severity, Code (a stable machine-readable identifier), and a
// human-readable Message. The String method renders it in compiler-style
// one-line form.
type Diagnostic = grammar.Diagnostic

// Severity classifies a [Diagnostic]'s seriousness. The scan never aborts on a
// Warning or Hint; the caller decides policy. Compare against [SeverityError],
// [SeverityWarning] and [SeverityHint].
type Severity = grammar.Severity

// Code is a stable, machine-readable identifier for a class of [Diagnostic]
// (e.g. "validate.unsupported-go-type", "scan.ignored-by-tag"). Codes are
// grouped by prefix: parse.* (lexer/parser), validate.* (semantic), scan.*
// (scan environment). Callers may switch on it to filter or route diagnostics.
type Code = grammar.Code

// Severity levels, ordered from most to least serious.
const (
SeverityError = grammar.SeverityError
SeverityWarning = grammar.SeverityWarning
SeverityHint = grammar.SeverityHint
)
21 changes: 21 additions & 0 deletions fixtures/enhancements/unsupported-go-type/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package unsupportedgotype carries a model with a field whose Go type
// cannot be represented in Swagger 2.0. The scanner drops the field and
// records a validate.unsupported-go-type warning (the diagnostic that
// replaced the legacy stderr "unsupported Go type" log line).
package unsupportedgotype

// Widget is a model with one representable field and one that codescan
// cannot translate.
//
// swagger:model Widget
type Widget struct {
// Name is a normal field.
Name string `json:"name"`

// Weird is a complex number — Swagger 2.0 has no such type, so the
// field is dropped with a validate.unsupported-go-type warning.
Weird complex128 `json:"weird"`
}
9 changes: 2 additions & 7 deletions internal/builders/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ The source files keep godoc concise; complex invariants, design trade-offs, and
(`schema`, `parameters`, `responses`, `routes`, `operations`, `spec`).

It owns the scanner context, the active declaration, the
parsed-block memoisation cache, the diagnostic accumulator, the
post-decl queue, and the slog logger.
parsed-block memoisation cache, the diagnostic accumulator, and the
post-decl queue.

---

Expand Down Expand Up @@ -152,11 +152,6 @@ shared with the parameters/responses field-signal scanners) and

These are real maintenance items the package author noted; they remain open for a future pass.

- **logger configurability.** `New` instantiates `slog.Default()`.
An option to accept a user-supplied `*slog.Logger` (level,
coloured output, structured fields) would let callers opt into a
consistent logging surface across builders. Currently every
builder's `Warn`/`Debug` writes through the global default.
- **`ireturn` on `ParseBlock`.** The `nolint:ireturn` directive on
`ParseBlock` carries because `grammar.Block` is a polymorphic
interface — that's the documented return type. The lint could
Expand Down
19 changes: 1 addition & 18 deletions internal/builders/common/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ package common
import (
"go/ast"
"go/token"
"log/slog"

"github.com/go-openapi/codescan/internal/ifaces"
"github.com/go-openapi/codescan/internal/parsers/grammar"
Expand All @@ -35,22 +34,16 @@ type Builder struct {
postDeclSet map[*ast.Ident]struct{} // dedup index keyed by EntityDecl.Ident
diagnostics []grammar.Diagnostic
blockCache map[*ast.CommentGroup][]grammar.Block
logger *slog.Logger
}

// New builds a [Builder] bound to ctx and decl.
//
// The blockCache is pre-allocated empty; logger defaults to [slog.Default].
//
// See [§quirks-open] for the planned configurability.
//
// [§quirks-open]: https://github.com/go-openapi/codescan/blob/master/internal/common/README.md#quirks-open
// The blockCache is pre-allocated empty.
func New(ctx *scanner.ScanCtx, decl *scanner.EntityDecl) *Builder {
return &Builder{
Ctx: ctx,
Decl: decl,
blockCache: make(map[*ast.CommentGroup][]grammar.Block),
logger: slog.Default(),
}
}

Expand All @@ -62,16 +55,6 @@ func (s *Builder) PostDeclarations() []*scanner.EntityDecl {
return s.postDecls
}

// Warn writes a warning to the Builder's slog logger.
func (s *Builder) Warn(msg string, args ...any) {
s.logger.Warn(msg, args...)
}

// Debug writes a debug message to the Builder's slog logger.
func (s *Builder) Debug(msg string, args ...any) {
s.logger.Debug(msg, args...)
}

// Diagnostics returns every grammar.Diagnostic accumulated by this Builder during a Build pass.
//
// Source order is preserved; no deduplication is applied.
Expand Down
9 changes: 0 additions & 9 deletions internal/builders/parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/go-openapi/codescan/internal/builders/schema"
"github.com/go-openapi/codescan/internal/builders/validations"
"github.com/go-openapi/codescan/internal/ifaces"
"github.com/go-openapi/codescan/internal/logger"
"github.com/go-openapi/codescan/internal/parsers/grammar"
"github.com/go-openapi/codescan/internal/scanner"
oaispec "github.com/go-openapi/spec"
Expand Down Expand Up @@ -66,7 +65,6 @@ func (p *Builder) Build(operations map[string]*oaispec.Operation) error {
operations[opid] = operation
operation.ID = opid
}
logger.DebugLogf(p.Ctx.Debug(), "building parameters for: %s", opid)
p.currentOpID = opid

// analyze struct body for fields etc
Expand All @@ -91,7 +89,6 @@ func (p *Builder) buildFromType(otpe types.Type, op *oaispec.Operation, seen map
case *types.Named:
return p.buildNamedType(tpe, op, seen)
case *types.Alias:
logger.DebugLogf(p.Ctx.Debug(), "alias(parameters.buildFromType): got alias %v to %v", tpe, tpe.Rhs())
return p.buildAlias(tpe, op, seen)
default:
return fmt.Errorf("unhandled type (%T): %s: %w", otpe, tpe.String(), ErrParameters)
Expand All @@ -107,7 +104,6 @@ func (p *Builder) buildNamedType(tpe *types.Named, op *oaispec.Operation, seen m

switch stpe := o.Type().Underlying().(type) {
case *types.Struct:
logger.DebugLogf(p.Ctx.Debug(), "build from named type %s: %T", o.Name(), tpe)
if decl, found := p.Ctx.DeclForType(o.Type()); found {
return p.buildFromStruct(decl, stpe, op, seen)
}
Expand Down Expand Up @@ -140,8 +136,6 @@ func (p *Builder) buildAlias(tpe *types.Alias, op *oaispec.Operation, seen map[s
}

func (p *Builder) buildFromField(fld *types.Var, tpe types.Type, typable ifaces.SwaggerTypable, seen map[string]oaispec.Parameter) error {
logger.DebugLogf(p.Ctx.Debug(), "build from field %s: %T", fld.Name(), tpe)

switch ftpe := tpe.(type) {
case *types.Basic:
return resolvers.SwaggerSchemaForType(ftpe.Name(), typable)
Expand All @@ -160,7 +154,6 @@ func (p *Builder) buildFromField(fld *types.Var, tpe types.Type, typable ifaces.
case *types.Named:
return p.buildNamedField(ftpe, typable)
case *types.Alias:
logger.DebugLogf(p.Ctx.Debug(), "alias(parameters.buildFromField): got alias %v to %v", ftpe, ftpe.Rhs())
return p.buildFieldAlias(ftpe, typable, fld, seen)
default:
return fmt.Errorf("unknown type for %s: %T: %w", fld.String(), fld.Type(), ErrParameters)
Expand Down Expand Up @@ -499,13 +492,11 @@ func (p *Builder) resolveParamType(signals fieldDocSignals, fld *types.Var, name
// Returns the parameter name if the field was processed, or "" if it was skipped.
func (p *Builder) processParamField(fld *types.Var, decl *scanner.EntityDecl, seen map[string]oaispec.Parameter) (string, error) {
if !fld.Exported() {
logger.DebugLogf(p.Ctx.Debug(), "skipping field %s because it's not exported", fld.Name())
return "", nil
}

afld := resolvers.FindASTField(decl.File, fld.Pos())
if afld == nil {
logger.DebugLogf(p.Ctx.Debug(), "can't find source associated with %s", fld.String())
return "", nil
}

Expand Down
12 changes: 0 additions & 12 deletions internal/builders/responses/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/go-openapi/codescan/internal/builders/resolvers"
"github.com/go-openapi/codescan/internal/builders/schema"
"github.com/go-openapi/codescan/internal/ifaces"
"github.com/go-openapi/codescan/internal/logger"
"github.com/go-openapi/codescan/internal/parsers/grammar"
"github.com/go-openapi/codescan/internal/scanner"
oaispec "github.com/go-openapi/spec"
Expand Down Expand Up @@ -68,7 +67,6 @@ func (r *Builder) Build(responses map[string]oaispec.Response) error {

name, _ := r.Decl.ResponseNames()
response := responses[name]
logger.DebugLogf(r.Ctx.Debug(), "building response: %s", name)

// Cross-ref linkage: anchor this response's headers and in:body schema under
// /responses/{name}. The response name is known here (no deferral, unlike a
Expand Down Expand Up @@ -156,8 +154,6 @@ func (r *Builder) bodyPathFor(typable ifaces.SwaggerTypable) string {
}

func (r *Builder) buildFromField(fld *types.Var, tpe types.Type, typable ifaces.SwaggerTypable, seen map[string]bool) error {
logger.DebugLogf(r.Ctx.Debug(), "build from field %s: %T", fld.Name(), tpe)

switch ftpe := tpe.(type) {
case *types.Basic:
return resolvers.SwaggerSchemaForType(ftpe.Name(), typable)
Expand All @@ -178,7 +174,6 @@ func (r *Builder) buildFromField(fld *types.Var, tpe types.Type, typable ifaces.
case *types.Named:
return r.buildNamedField(ftpe, typable)
case *types.Alias:
logger.DebugLogf(r.Ctx.Debug(), "alias(responses.buildFromField): got alias %v to %v", ftpe, ftpe.Rhs())
return r.buildFieldAlias(ftpe, typable, fld, seen)
default:
return fmt.Errorf("unknown type for %s: %T: %w", fld.String(), fld.Type(), ErrResponses)
Expand Down Expand Up @@ -255,7 +250,6 @@ func (r *Builder) buildFromType(otpe types.Type, resp *oaispec.Response, seen ma
case *types.Named:
return r.buildNamedType(tpe, resp, seen)
case *types.Alias:
logger.DebugLogf(r.Ctx.Debug(), "alias(responses.buildFromType): got alias %v to %v", tpe, tpe.Rhs())
return r.buildAlias(tpe, resp, seen)
default:
return fmt.Errorf("anonymous types are currently not supported for responses: %w", ErrResponses)
Expand All @@ -271,7 +265,6 @@ func (r *Builder) buildNamedType(tpe *types.Named, resp *oaispec.Response, seen

switch stpe := o.Type().Underlying().(type) {
case *types.Struct:
logger.DebugLogf(r.Ctx.Debug(), "build from type %s: %T", o.Name(), tpe)
if decl, found := r.Ctx.DeclForType(o.Type()); found {
return r.buildFromStruct(decl, stpe, resp, seen)
}
Expand Down Expand Up @@ -431,7 +424,6 @@ func (r *Builder) buildFromStruct(decl *scanner.EntityDecl, tpe *types.Struct, r
}

if fld.Anonymous() {
logger.DebugLogf(r.Ctx.Debug(), "skipping anonymous field")
continue
}

Expand Down Expand Up @@ -500,20 +492,17 @@ func (r *Builder) buildBodyEmbed(fld *types.Var, resp *oaispec.Response, seen ma

func (r *Builder) processResponseField(fld *types.Var, decl *scanner.EntityDecl, resp *oaispec.Response, seen map[string]bool) error {
if !fld.Exported() {
logger.DebugLogf(r.Ctx.Debug(), "skipping field %s because it's not exported", fld.Name())
return nil
}

afld := resolvers.FindASTField(decl.File, fld.Pos())
if afld == nil {
logger.DebugLogf(r.Ctx.Debug(), "can't find source associated with %s", fld.String())
return nil
}

signals := scanFieldDocSignals(r.ParseBlocks(afld.Doc), afld.Doc)

if signals.ignored {
logger.DebugLogf(r.Ctx.Debug(), "field %v is deliberately ignored", fld)
return nil
}

Expand Down Expand Up @@ -593,7 +582,6 @@ func (r *Builder) processResponseField(fld *types.Var, decl *scanner.EntityDecl,
resp.Schema = &oaispec.Schema{}
resp.Schema.Typed("file", "")
} else {
logger.DebugLogf(r.Ctx.Debug(), "build response %v (%v) (not a file)", fld, fld.Type())
var refAttempted bool
if err := r.buildFromField(fld, fld.Type(), responseTypable{
in: in,
Expand Down
4 changes: 2 additions & 2 deletions internal/builders/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,8 @@ discriminator hint downstream go-swagger consumes.

Strips pointers (recurses), routes `*types.Named` through
`buildNamedAllOf`, routes `*types.Alias` through `buildAlias`. Any
other input is dropped silently with a `logger.UnsupportedTypeKind`
warning — parity with v1, which had no surface for non-Named /
other input is dropped with a `validate.unsupported-go-type` Warning
diagnostic (`warnUnsupportedGoType`) — v1 had no surface for non-Named /
non-Alias allOf members.

### `buildNamedAllOf` — symmetric arm dispatch
Expand Down
5 changes: 2 additions & 3 deletions internal/builders/schema/allof.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"go/types"

"github.com/go-openapi/codescan/internal/builders/resolvers"
"github.com/go-openapi/codescan/internal/logger"
"github.com/go-openapi/codescan/internal/scanner"
oaispec "github.com/go-openapi/spec"
)
Expand Down Expand Up @@ -156,7 +155,7 @@ func (s *Builder) buildAllOf(tpe types.Type, schema *oaispec.Schema) error {
tgt := NewTypable(schema, 0, s.skipExtensions)
return s.buildAlias(ftpe, tgt)
default:
logger.UnsupportedTypeKind("buildAllOf", ftpe)
s.warnUnsupportedGoType("buildAllOf", ftpe)
return nil
}
}
Expand Down Expand Up @@ -197,7 +196,7 @@ func (s *Builder) buildNamedAllOf(ftpe *types.Named, schema *oaispec.Schema) err
case *types.Interface:
return s.buildFromInterface(decl, utpe, schema, make(map[string]propOwner))
default:
logger.UnsupportedTypeKind("buildNamedAllOf", utpe)
s.warnUnsupportedGoType("buildNamedAllOf", utpe)
return nil
}
}
Loading
Loading