common

package
v0.35.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 21, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

common builder — maintainer notes

This document is the long-form companion to the common.Builder code.

The source files keep godoc concise; complex invariants, design trade-offs, and intentionally-deferred follow-ups live here.

common.Builder is the shared state every per-decl builder embeds (schema, parameters, responses, routes, operations, spec).

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


Table of contents

  • §blockcacheParseBlock / ParseBlocks memoisation strategy and scope
  • §makeref — why MakeRef lives on the common base
  • §diagnostics — accumulator ordering, dedup posture, LSP-evolution caveat
  • §postdecls — per-Builder dedup index + cross-Builder re-dedup in the orchestrator
  • §embed-inheritance — annotations on an embed flow to promoted members
  • §quirks-open — deferred follow-ups

§blockcache — ParseBlock / ParseBlocks memoisation

Builder.blockCache memoises grammar.NewParser(...).ParseAll(cg) results keyed by *ast.CommentGroup pointer. Two reasons:

  1. Recursive type descent re-visits the same comment. A struct field whose type is itself a struct triggers a nested buildFromDecl/buildFromType pass; without memoisation each level re-lexes and re-parses the same field-doc comment group.
  2. Multi-annotation visibility. ParseAll yields one Block per annotation on the comment group (the swagger:type + swagger:model co-decl is the canonical double-annotation case). Callers that only need the first annotation use ParseBlock; callers that need every annotation iterate ParseBlocks.

The cache is per-Builder (one top-level decl build), so no synchronisation is needed: a Builder is single-goroutine for its entire lifetime. Crossing a Builder boundary discards the cache, which is fine — the scanner context owns the FileSet, so a parser constructed in a sibling Builder still produces position-stable output.

ParseBlock(cg) always returns a non-nil Block (the parser yields at least one Block even for a nil comment group, conventionally an UnboundBlock). Callers can read AnnotationKind(), AnnotationArg(), etc. on the result unconditionally.

§makeref — why MakeRef lives on the common base

MakeRef writes $ref: #/definitions/<name> onto a target via SwaggerTypable.SetRef, then enqueues the referenced declaration on the Builder's post-decl queue so the spec orchestrator visits it during the discovery loop.

The name source is decl.Names() (first entry — top-level decls in this codebase have a single name).

The method lives on common.Builder rather than per-package because every builder needs the same operation with the same side effect. Hoisting also means future cross-cutting refinements — a name-collision diagnostic, a discovery-loop instrumentation counter, a guard against emitting $ref to an unexported name — are one-place edits.

§diagnostics — accumulator + LSP-evolution caveat

Diagnostics() returns every accumulated grammar.Diagnostic in source order, raw — no deduplication. The build re-processes the same field/annotation across passes (most visibly a swagger:parameters struct applied to several operation ids, which rebuilds every field once per id), so the slice can carry the identical diagnostic more than once.

The OnDiagnostic callback stream is deduped, however: ScanCtx.EmitDiagnostic suppresses exact duplicates — same position, code and message — for the lifetime of one scan, so a consumer that only listens on the callback (the TUI, an LSP server) sees each distinct diagnostic once. This is the "structural deduplication" the caveat below anticipated; it lives at the single delivery boundary so the raw accumulator stays available for callers that want every occurrence.

The diagnostic surface is experimental and expected to evolve further once the LSP integration matures. Likely remaining changes: typed severity classes and per-position provenance. The shape is conservative today (slice of grammar.Diagnostic + a callback hook) precisely so it can be widened without breaking callers.

RecordDiagnostic appends to the slice and delivers through Ctx.EmitDiagnostic (the deduped boundary) when a sink is wired. Walkers' Diagnostic callback points at this method so grammar-level warnings flow into the same accumulator.

§postdecls — dedup index + orchestrator re-dedup

AppendPostDecl(decl) enqueues decl for post-processing by the spec orchestrator's discovery loop. The Builder maintains a per-instance dedup index (postDeclSet, keyed by *ast.Ident) so a single decl re-discovered N times during one Build pass only enqueues once.

A SECOND dedup runs in spec.Builder.buildDiscovered at consumption time, because two different per-decl Builders may surface the same post-decl independently. The double-guard means a discovered decl never reaches a second Build pass even when sibling Builders race to register it.

Nil and Ident-less decls are silently ignored — defensive against the scanner emitting partial decls during error recovery.

ResetPostDeclarations() drops the whole queue for a Build pass. Its one caller is the schema builder's SimpleSchema catch-at-exit validator (go-swagger#1088): when a non-body parameter / response-header element dissolves an illegal $ref, the decl MakeRef discovered for that ref is a byproduct of the now-removed reference and must not linger as an orphan definition. A single-type Build renders exactly one target, so every queued decl is reachable only through it; clearing the whole queue is correct, and a decl genuinely referenced elsewhere is re-discovered by that other site's Builder and deduplicated here and in the orchestrator.

§embed-inheritance — annotations on an embed flow to promoted members

EmbedInheritance + ReadEmbedInheritance are the shared kernel of the rule "a doc-comment directive on an embedded (anonymous) struct field applies to the members that embed promotes" (go-swagger#2701). All three field-walking builders embed *common.Builder and use it so the behaviour is identical:

  • parameters consume In and Required (an in: path / required: on the embed flows to the promoted parameters);
  • schema consumes Required (a required: on the embed adds the promoted properties to the enclosing object's required list); it has no in: concept;
  • responses consume In (the body/header routing discriminator); OAS2 response headers carry no required.

Each builder keeps its own struct walk (the output objects differ — Parameter vs Header vs schema property), threading the context with save/restore around its embed recursion: the embed's own directive wins over the inherited one, an absent directive carries the parent's through (so nesting accumulates), and a promoted member's own directive always wins over the inherited fallback. ScanInLocation (the in: line scan, shared with the parameters/responses field-signal scanners) and grammar.NormalizeIn back the In half.

§quirks-open — deferred follow-ups

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

  • 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 be disabled package-wide rather than per-function; consider as a .golangci.yml exclusion once the broader lint posture is reviewed.

Documentation

Overview

Package common holds shared per-Builder state every concrete per-decl builder (schema, parameters, responses, routes, operations, spec) embeds. See [./README.md](./README.md) for the long-form maintainer notes on cache scope, diagnostic posture, and the post-decl queue's double-dedup design.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ScanInLocation added in v0.35.0

func ScanInLocation(text string) (value string, valid bool, invalid string)

ScanInLocation finds the first `in: X` line in text and returns the canonical OAS v2 form of X (when recognised, case-insensitive via grammar.NormalizeIn) or the raw candidate (when present but out-of-vocabulary). The `form` alias is NOT accepted here — it is contained to the routes inline-param path.

Shared by the parameters and responses field-signal scanners and by Builder.ReadEmbedInheritance; `in:` is line-scanned rather than read as a grammar Property because grammar attaches pre-annotation lines to the following annotation's prose, not its property list.

Types

type Builder

type Builder struct {
	Ctx  *scanner.ScanCtx
	Decl *scanner.EntityDecl
	// contains filtered or unexported fields
}

Builder holds the per-decl state shared across every concrete builder via embedding.

See §blockcache, §diagnostics, and §postdecls for the cache scope, accumulator posture, and discovery queue's dedup design.

func New

func New(ctx *scanner.ScanCtx, decl *scanner.EntityDecl) *Builder

New builds a Builder bound to ctx and decl.

The blockCache is pre-allocated empty.

func (*Builder) AppendPostDecl

func (s *Builder) AppendPostDecl(decl *scanner.EntityDecl)

AppendPostDecl marks decl for post-processing by the spec orchestrator's discovery loop. Idempotent per-Builder: re-appending a decl whose Ident was already seen is a no-op. Nil and Ident-less decls are silently ignored.

Details

See [§postdecls](./README.md#postdecls) — per-Builder dedup index and the second dedup applied at consumption time by spec.Builder.buildDiscovered.

func (*Builder) Diagnostics

func (s *Builder) Diagnostics() []grammar.Diagnostic

Diagnostics returns every grammar.Diagnostic accumulated by this Builder during a Build pass.

Source order is preserved; no deduplication is applied. The slice is owned by the Builder; callers must not mutate it. Returns nil before Build is invoked or when no diagnostics were recorded.

Details

See [§diagnostics](./README.md#diagnostics) — accumulator ordering, dedup posture, and the LSP-evolution caveat (the diagnostic surface is expected to widen once IDE integration matures).

func (*Builder) MakeRef

func (s *Builder) MakeRef(decl *scanner.EntityDecl, prop ifaces.SwaggerTypable) error

MakeRef writes a `$ref: "#/definitions/<name>"` onto prop and registers decl with the discovery loop via AppendPostDecl. The name comes from decl.Names() (the first entry — top-level decls in this codebase have a single name). Returns an error only if oaispec.NewRef rejects the JSON pointer.

Details

See [§makeref](./README.md#makeref) — why the operation lives on the common base and what kinds of cross-cutting refinements that shape enables.

func (*Builder) ParseBlock

func (s *Builder) ParseBlock(cg *ast.CommentGroup) grammar.Block

ParseBlock returns the first Block from Builder.ParseBlocks.

This is the "primary" annotation for callers that don't need multi-annotation visibility (title/description, field-level lookups).

func (*Builder) ParseBlocks

func (s *Builder) ParseBlocks(cg *ast.CommentGroup) []grammar.Block

ParseBlocks returns the cached grammar.Block slice for cg (one entry per annotation), parsing on first access and memoising the result.

Always returns a non-nil slice with at least one Block, so consumers can call [Block.AnnotationKind], [Block.AnnotationArg] / etc. unconditionally on the first element.

Details

See [§blockcache](./README.md#blockcache) — memoisation scope, why ParseAll is preferred over Parse, and the per-Builder (single-goroutine) lifetime that obviates synchronisation.

func (*Builder) PostDeclarations

func (s *Builder) PostDeclarations() []*scanner.EntityDecl

PostDeclarations returns the post-decl queue accumulated by this Builder during a Build pass, in source order.

See [§postdecls](./README.md#postdecls).

func (*Builder) ReadEmbedInheritance added in v0.35.0

func (s *Builder) ReadEmbedInheritance(doc *ast.CommentGroup, current EmbedInheritance) EmbedInheritance

ReadEmbedInheritance merges the `in:`/`required:` directives on an embedded field's doc comment into current, returning the context to pass down to the fields that embed promotes. The embed's own directives override the inherited ones; absent directives carry current through unchanged.

doc is the embed field's *ast.CommentGroup (nil → current unchanged).

func (*Builder) RecordDiagnostic

func (s *Builder) RecordDiagnostic(d grammar.Diagnostic)

RecordDiagnostic accumulates one diagnostic on the Builder and fires the consumer's [Options.OnDiagnostic] callback when wired.

Walker.Diagnostic is bound to this method, so grammar-level warnings flow through the same accumulator as builder-level ones.

func (*Builder) ResetPostDeclarations added in v0.35.0

func (s *Builder) ResetPostDeclarations()

ResetPostDeclarations drops every decl this Builder enqueued during the current Build pass. Used by the SimpleSchema catch-at-exit validator: when a non-body parameter / response-header element dissolves an illegal $ref, the decl that MakeRef discovered for that ref is a byproduct of the now-removed reference and would otherwise linger as an orphan definition (go-swagger#1088). A single-type Build renders exactly one target, so every queued decl is reachable only through it; clearing the whole queue is correct. A decl genuinely referenced elsewhere is re-discovered by that other site's Builder and deduplicated by the orchestrator.

Details

See [§postdecls](./README.md#postdecls).

func (*Builder) WarnStrippedPathRegex added in v0.35.0

func (s *Builder) WarnStrippedPathRegex(pos token.Pos, params []string)

WarnStrippedPathRegex records a warning that one or more inline regex path-parameter constraints (`{id:[0-9]+}`) were stripped to the bare `{id}` template form. OpenAPI 2.0 path templating follows RFC 6570 URI Template Level-1 expansion (simple `{name}` substitution) only — it cannot express regex/operator constraints — so the route is still emitted, with the constraint dropped. No-op when params is empty. Shared by the routes and operations builders.

type EmbedInheritance added in v0.35.0

type EmbedInheritance struct {
	In          string
	InSet       bool
	Required    bool
	RequiredSet bool
}

EmbedInheritance carries the doc-comment directives an embedded (anonymous) struct field passes down to the members it promotes: the `in:` location and the `required:` flag. It is the shared kernel of the "annotations on an embed apply to its promoted members" rule, used by the schema, parameters, and responses builders so the behaviour is identical everywhere. See go-swagger#2701.

Semantics:

  • A member's own `in:`/`required:` always wins; the inherited value is a fallback the member consults only when it sets nothing itself.
  • An embed that omits a directive leaves the corresponding inherited value unchanged, so nesting accumulates (an inner embed without its own `in:` keeps the outer one).
  • `in:` is meaningful only to the parameters and responses builders (the schema builder has no location concept); `required:` is meaningful to the parameters and schema builders (OAS2 response headers carry no `required`).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL