Documentation
¶
Overview ¶
Package formatting formats generated Go source.
Format parses the source, drops the imports nothing uses, sorts and groups the rest, rejects the ones that contradict each other, and prints the result to an io.Writer:
err := formatting.Format(file, rendered,
formatting.WithImportGroups("github.com/go-openapi", baseImport),
)
Rendering many files ¶
A generator renders a template into a buffer and formats what it rendered. Reset one buffer and use it again rather than allocating one per template: Format takes a bytes.Buffer as its source, reads its bytes without copying them and leaves it untouched.
var rendered bytes.Buffer
for _, tpl := range templates {
rendered.Reset()
if err := tpl.Execute(&rendered, data); err != nil {
return err
}
if err := formatting.Format(out, &rendered, groups); err != nil {
return err
}
}
That saves about a tenth of the memory a file costs to render and format. Passing rendered.Bytes() does the same thing; passing an io.Reader could not, which is why Source does not admit one.
Imports are never resolved ¶
Format removes an import no code uses. It never adds one. goimports resolves a missing import by searching the module cache and the build list, which makes the output depend on the machine that ran the generator: the same template and the same spec produce different files, and a maintainer cannot reproduce what a user reports. A template writes the imports its code needs, and code that names a package it did not import fails to compile, which is a better answer than a guess.
What pruning can know ¶
The identifier an import binds is the imported package's own package clause, and the path only says where to find it. "github.com/json-iterator/go" declares jsoniter, "github.com/prometheus/client_model/go" declares io_prometheus_client, and reading either name means loading the package. So Format deletes an import only when it knows the name, from one of three places:
- an alias, which states the name in the source;
- the standard library, held in a generated table built from "go list std";
- WithResolvedImports, where the caller states the name.
A bare third-party import is a guess. A guess keeps an import when one of the names it could declare appears as a qualifier, and never deletes one, so this survives:
import "github.com/go-openapi/strfmt" // nothing writes strfmt., and the import stays
Two ways to get it deleted. WithForceImportsPruning promises every bare import declares the name ImportedPackageName gives. WithResolvedImports states the awkward names instead of promising there are none, and a map built once serves every build.
A third way costs no option at all: write the alias even where it repeats the package name.
import strfmt "github.com/go-openapi/strfmt" // the binding is certain, so the import is pruned
gofmt leaves that alone, and only revive's redundant-import-alias rule reports it, which is off by default. A generator holding either the promise or the map needs it only for the packages that break the convention, where the alias is not redundant and writing it is ordinary Go.
WithSimplifiedImportAliases takes such an alias back out once the name is proven, so a template may write every alias for safety and hand the reader ordinary Go. It drops nothing on a guess, and nothing whose alias the path cannot replace.
A blank import runs an init and binds no qualifier, so it is never pruned. A dot import spills its names into the file scope and no qualifier ever appears, so nothing about it can be checked: use goimports on a file that relies on them.
What Format does not check ¶
Format reads one file's syntax and nothing else. An import of another module's internal package formats like any other, a //go:build line is copied through untouched, and config_linux.go is formatted the same as any other file. The go compiler enforces those rules, and Format does not duplicate them.
The imports report ¶
Format returns an ImportsReport beside its error, holding one ImportRecord per import: the path, the name it binds, whether that name was stated or guessed, and what became of it.
report, err := formatting.Format(out, rendered)
if err != nil {
return err
}
if report.HasImportsInDoubt() {
log.Printf("could not name: %v", report.PathsInDoubt())
}
A report with nothing in doubt means every import was decided, so the output holds no import the file does not use. Anything in doubt is a path to resolve: github.com/go-openapi/codegen/formatting/resolve reads the names from the packages themselves and returns the map WithResolvedImports takes. Run it once, commit the map, and every build agrees.
A clash between names Format knows is an error. A clash between guessed names may not be real — either package may declare something the path does not show — so those imports come back as ImportCollision and neither is pruned.
Duplicate imports ¶
The blank lines a template wrote inside its import block mean nothing to Format: it sorts the whole block at once and keeps one spec per path. A template that writes
import ( "bytes" "bytes" "context" )
gets "bytes" and "context" back, in one group.
gofmt and goimports both answer differently. They sort each blank-line-separated run on its own and never move an import between runs, so both keep the second "bytes" and the file fails to compile with "bytes redeclared in this block". A template assembling its imports from several fragments hits this whenever two fragments contribute the same package.
Inconsistent imports ¶
Two imports left after pruning may still contradict each other, and Format returns ErrInconsistentImports rather than print a file the caller has to debug:
- one package under two names, as "bytes" beside b "bytes". The go compiler accepts it; the code reads as though b and bytes were different packages.
- one name bound to two packages, as "crypto/rand" beside "math/rand" in a file writing rand.Read. The go compiler rejects it.
A name is compared as the file writes it: an alias is the name it declares, and a bare import binds whichever of its guessed names the file writes as a qualifier. So "crypto/rand" beside "math/rand" passes when nothing writes rand. — pruning takes both — and "github.com/go-openapi/core" beside "k8s.io/api/core/v1" passes when the file writes both core. and v1., because then the two bind different names.
One error names every mismatch, so a template with three bad imports is fixed in one pass. _ and . bind no qualifier and are left alone, and so is an import whose package Format cannot name.
Naming an import ¶
ImportedPackageName returns the identifier to qualify an import path with, version elements dropped, so a generator can name an import it is about to write. Format settles the other question — which name an existing import already binds — for itself, and reports what it could not settle.
Grouping ¶
Without WithImportGroups the output has two groups, the standard library and everything else. Each prefix passed to WithImportGroups adds a group between them, in the order given:
WithImportGroups("github.com/go-openapi", "example.com/petstore")
import (
"context" // standard library
"github.com/go-openapi/runtime"
"example.com/petstore/models"
"github.com/google/uuid" // everything else
)
The prefixes travel with the call, so two goroutines may format with different grouping.
A later gofmt or goimports keeps this layout. Both sort each blank-line-separated run of imports on its own and never move an import from one run into another, so they leave the groups where Format put them, and a "golangci-lint fmt" over generated code changes nothing. gci is the exception: it enforces one order over the whole block and regroups.
gofumpt ¶
WithGoFumpt applies the gofumpt rules before printing. gofumpt is an optional dependency and lives in its own module, so a build that does not ask for it does not pay for it. Enable it with a blank import:
import _ "github.com/go-openapi/codegen/formatting/enable/gofumpt"
Without that import, WithGoFumpt makes Format return ErrNoGoFumpt.
Line endings ¶
Format writes \n. go/printer offers no way to ask for anything else, so a source written with \r\n comes back with \n, exactly as gofmt rewrites it. A fragment is the one exception: the bytes that surrounded it are put back as they were written, so its \r\n survive around a body that uses \n. go/format.Source answers the same.
Fragments ¶
A source with no package clause is parsed as a declaration list, then as a statement list, the way go/format.Source does. A fragment cannot stream: Format prints it to a buffer, strips the wrapping and restores the surrounding white space before writing.
Index ¶
- func ImportedPackageName(importPath string) string
- type Error
- type ImportRecord
- type ImportStatus
- type ImportsReport
- func (r *ImportsReport) HasImportsInDoubt() bool
- func (r *ImportsReport) Imports() []ImportRecord
- func (r *ImportsReport) InDoubt() []ImportRecord
- func (r *ImportsReport) PathsInDoubt() []string
- func (r *ImportsReport) Pruned() []ImportRecord
- func (r *ImportsReport) String() string
- func (r *ImportsReport) Used() []ImportRecord
- type Option
- type Source
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ImportedPackageName ¶
ImportedPackageName returns the single identifier a generator should qualify importPath with.
A version element is never the answer, so "k8s.io/api/apps/v1" and "k8s.io/api/core/v1" give apps and core instead of v1 twice. That is the point: two imports of the same API version collide under the name they declare and not under this one.
The package at that path really does declare v1, so write the name as an alias whenever the path carries a version element:
apps "k8s.io/api/apps/v1"
It returns "" when no part of the path is a legal Go identifier, as for "example.com/2fa".
Use [importedPackageNames] to ask the other question, which name an existing import is already bound to. The two disagree exactly where an alias is needed.
Example ¶
ExampleImportedPackageName shows a generator naming the imports it is about to write.
The two Kubernetes packages both declare v1, so they collide under that name. The alias carries the name this function picked into the file.
package main
import (
"fmt"
"github.com/go-openapi/codegen/formatting"
)
func main() {
for _, importPath := range []string{
"context",
"k8s.io/api/apps/v1",
"k8s.io/api/core/v1",
"github.com/go-openapi/testify/v2",
} {
fmt.Printf("%s %q\n", formatting.ImportedPackageName(importPath), importPath)
}
}
Output: context "context" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" testify "github.com/go-openapi/testify/v2"
Types ¶
type Error ¶
type Error string
Error is a string that implements error, so a sentinel below can be a constant.
const ( // ErrFormat matches every error [Format] returns. ErrFormat Error = "formatting error" // ErrInconsistentImports is returned when the imports left after pruning contradict one another: // one package imported under two names, or one name bound to two packages. The message names // every mismatch [Format] found, and nothing is printed. ErrInconsistentImports Error = "inconsistent imports" // ErrNoGoFumpt is returned when [WithGoFumpt] is passed but the gofumpt rules were never // registered. Blank-import github.com/go-openapi/codegen/formatting/enable/gofumpt. ErrNoGoFumpt Error = "gofumpt requested but not enabled: blank-import " + `_ "github.com/go-openapi/codegen/formatting/enable/gofumpt"` )
type ImportRecord ¶
type ImportRecord struct {
// Path is the import path, unquoted.
Path string
// Alias is the name written before the path, empty for a bare import. "_" and "." appear here as
// they were written.
Alias string
// Name is the qualifier the import binds, empty when [Format] could not tell. Under
// [ImportCollision], the records that clash share a Name.
Name string
// Certain reports whether Name was stated rather than guessed: written as an alias, held in the
// standard library table, or supplied through [WithResolvedImports].
Certain bool
Status ImportStatus
}
ImportRecord is what Format made of one import.
func (ImportRecord) String ¶
func (r ImportRecord) String() string
type ImportStatus ¶
type ImportStatus int
ImportStatus says what became of one import.
const ( // ImportUsed marks an import the file writes as a qualifier. It stays. ImportUsed ImportStatus = iota // ImportPruned marks an import [Format] deleted, because its name is known and nothing used it. ImportPruned // ImportInDoubt marks a bare import whose package [Format] cannot name, so it cannot say whether // the file uses it. It stays. Resolve it with [WithResolvedImports], or accept the guess with // [WithForceImportsPruning]. ImportInDoubt // ImportCollision marks an import whose guessed name is claimed by another import as well. It // stays, and neither of the two is pruned. A collision between names [Format] knows is an error // rather than a status: see [ErrInconsistentImports]. ImportCollision // ImportBlank marks a _ import. It runs an init, binds no qualifier, and is never pruned. ImportBlank // ImportDot marks a . import. Its names land in the file scope and no qualifier appears, so // nothing about it can be checked. It is never pruned and always in doubt. ImportDot // ImportCgo marks the pseudo-package C, which carries a cgo preamble. Nothing may touch it. ImportCgo )
func (ImportStatus) String ¶
func (s ImportStatus) String() string
type ImportsReport ¶
type ImportsReport struct {
// contains filtered or unexported fields
}
ImportsReport accounts for every import Format read.
Format returns one whenever it parsed the source, including when it then failed, so a caller can see the imports behind an ErrInconsistentImports.
The report is the input to the resolving loop: format once, ask ImportsReport.HasImportsInDoubt, resolve ImportsReport.PathsInDoubt with github.com/go-openapi/codegen/formatting/resolve, and format again with WithResolvedImports until nothing is left in doubt.
Example ¶
ExampleImportsReport shows what Format could and could not decide.
package main
import (
"fmt"
"io"
"log"
"github.com/go-openapi/codegen/formatting"
)
func main() {
const src = `package p
import (
"bytes"
_ "embed"
"strings"
sf "github.com/go-openapi/swag"
"github.com/go-openapi/strfmt"
"github.com/json-iterator/go"
)
var (
_ bytes.Buffer
_ = jsoniter.Marshal
)
`
report, err := formatting.Format(io.Discard, []byte(src))
if err != nil {
log.Fatal(err)
}
fmt.Println(report)
fmt.Println()
fmt.Println("in doubt:", report.PathsInDoubt())
}
Output: bytes (bytes) used embed (_) blank github.com/go-openapi/strfmt (?) in doubt github.com/go-openapi/swag (sf) pruned github.com/json-iterator/go (?) in doubt strings (strings) pruned in doubt: [github.com/go-openapi/strfmt github.com/json-iterator/go]
func Format ¶
Format formats Go source and writes the result to w.
It drops the imports nothing uses, sorts and groups the rest, and prints in gofmt style. It never adds an import: see the package documentation for why. The blank lines the source wrote inside its import block are ignored, so a path written in two groups is one import in the output.
It returns ErrInconsistentImports when the imports left after pruning contradict each other — one package under two names, or one name bound to two packages.
The ImportsReport accounts for every import: what was pruned, what stayed, and what stayed only because Format could not name the package. It comes back whenever the source parsed, an ErrInconsistentImports included, and is nil only when parsing failed. Ask ImportsReport.HasImportsInDoubt before trusting that pruning was exact.
Passing a bytes.Buffer hands over its bytes and leaves it as it was: Format does not drain it, so a caller rendering one template after another resets it and writes the next.
The source is parsed, pruned, sorted, grouped and checked before a byte is written, so a source that does not parse or whose imports contradict each other leaves w untouched. Once printing starts only w itself can fail, and a fragment is printed to a buffer and copied in one write.
Example ¶
package main
import (
"log"
"os"
"github.com/go-openapi/codegen/formatting"
)
// rendered stands for what a template produced: misformatted, unsorted, and importing more than the
// code uses.
const rendered = `package petstore
import (
"context"
"github.com/go-openapi/strfmt"
"strings"
"github.com/go-openapi/swag/conv"
)
func New(ctx context.Context) *strfmt.DateTime {
_ = ctx
_ = conv.Pointer(1)
return nil
}
`
func main() {
if _, err := formatting.Format(os.Stdout, []byte(rendered)); err != nil {
log.Fatal(err)
}
}
Output: package petstore import ( "context" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/conv" ) func New(ctx context.Context) *strfmt.DateTime { _ = ctx _ = conv.Pointer(1) return nil }
func (*ImportsReport) HasImportsInDoubt ¶
func (r *ImportsReport) HasImportsInDoubt() bool
HasImportsInDoubt reports whether any import was kept because Format could not name it.
A false answer means every import was decided: pruning was exact, and the output has no import the file does not use.
Example ¶
ExampleImportsReport_HasImportsInDoubt shows the check worth making before trusting the output.
package main
import (
"fmt"
"io"
"log"
"github.com/go-openapi/codegen/formatting"
)
func main() {
const settled = `package p
import "bytes"
var _ bytes.Buffer
`
report, err := formatting.Format(io.Discard, []byte(settled))
if err != nil {
log.Fatal(err)
}
// nothing left in doubt: pruning was exact, and the output holds no import the file does not use
fmt.Println(report.HasImportsInDoubt())
}
Output: false
func (*ImportsReport) Imports ¶
func (r *ImportsReport) Imports() []ImportRecord
Imports returns every import, ordered by path.
func (*ImportsReport) InDoubt ¶
func (r *ImportsReport) InDoubt() []ImportRecord
InDoubt returns the imports Format kept because it could not name them.
That covers ImportInDoubt, ImportCollision and ImportDot: a name guessed and unmatched, a name guessed and claimed twice, and a dot import, which no qualifier ever reveals.
func (*ImportsReport) PathsInDoubt ¶
func (r *ImportsReport) PathsInDoubt() []string
PathsInDoubt returns the import paths of ImportsReport.InDoubt, ready for a resolver.
func (*ImportsReport) Pruned ¶
func (r *ImportsReport) Pruned() []ImportRecord
Pruned returns the imports Format deleted.
func (*ImportsReport) String ¶
func (r *ImportsReport) String() string
String summarises the report, one import per line.
func (*ImportsReport) Used ¶
func (r *ImportsReport) Used() []ImportRecord
Used returns the imports the file writes as a qualifier.
type Option ¶
type Option func(options) options
Option configures Format.
func WithForceImportsPruning ¶
func WithForceImportsPruning() Option
WithForceImportsPruning prunes an unused import even when its name was only guessed.
Passing it is a promise: every import in the source either carries an alias, or declares the name ImportedPackageName gives for its path — the last path element, with a /v2 or later suffix dropped and the last segment taken from a hyphenated element. Idiomatic packages keep that promise. "github.com/json-iterator/go" declares jsoniter and breaks it, and such an import is then pruned although the file uses it.
Without this option the formatter keeps a bare third-party import it cannot name, and reports it as in doubt.
Pass WithResolvedImports alongside to cover the imports the promise does not. A name given there is used instead of the guess, so one awkward dependency does not cost the promise:
formatting.Format(out, src,
formatting.WithForceImportsPruning(),
formatting.WithResolvedImports(map[string]string{
"github.com/json-iterator/go": "jsoniter",
}),
)
Example ¶
ExampleWithForceImportsPruning shows what the promise buys, on an import nothing uses.
package main
import (
"fmt"
"log"
"os"
"github.com/go-openapi/codegen/formatting"
)
func main() {
const src = `package p
import (
"bytes"
"github.com/go-openapi/strfmt"
)
var _ bytes.Buffer
`
show := func(label string, opts ...formatting.Option) {
fmt.Println(label)
if _, err := formatting.Format(os.Stdout, []byte(src), opts...); err != nil {
log.Fatal(err)
}
}
show("// strfmt stays: nothing states what that package declares")
show("// and goes once the caller promises it follows the convention",
formatting.WithForceImportsPruning())
}
Output: // strfmt stays: nothing states what that package declares package p import ( "bytes" "github.com/go-openapi/strfmt" ) var _ bytes.Buffer // and goes once the caller promises it follows the convention package p import ( "bytes" ) var _ bytes.Buffer
func WithGoFumpt ¶
func WithGoFumpt() Option
WithGoFumpt applies the gofumpt rules before printing.
Blank-import github.com/go-openapi/codegen/formatting/enable/gofumpt to make the rules available. Without it Format returns ErrNoGoFumpt rather than printing without them.
func WithImportGroups ¶
WithImportGroups adds one import group per prefix, between the standard library and the rest.
An import belongs to the first prefix it starts with, so pass the more specific prefix first. Without this option the output has two groups: the standard library, then everything else.
Example ¶
package main
import (
"log"
"os"
"github.com/go-openapi/codegen/formatting"
)
// rendered stands for what a template produced: misformatted, unsorted, and importing more than the
// code uses.
const rendered = `package petstore
import (
"context"
"github.com/go-openapi/strfmt"
"strings"
"github.com/go-openapi/swag/conv"
)
func New(ctx context.Context) *strfmt.DateTime {
_ = ctx
_ = conv.Pointer(1)
return nil
}
`
func main() {
if _, err := formatting.Format(os.Stdout, []byte(rendered),
formatting.WithImportGroups("github.com/go-openapi/swag", "github.com/go-openapi"),
); err != nil {
log.Fatal(err)
}
}
Output: package petstore import ( "context" "github.com/go-openapi/swag/conv" "github.com/go-openapi/strfmt" ) func New(ctx context.Context) *strfmt.DateTime { _ = ctx _ = conv.Pointer(1) return nil }
func WithResolvedImports ¶
WithResolvedImports states the name each import path declares, for the paths no rule can guess.
"github.com/json-iterator/go" declares jsoniter and "github.com/prometheus/client_model/go" declares io_prometheus_client; nothing in either path says so. A name given here is treated as certain, so the import is pruned when unused and never reported as in doubt.
A path appears in at most one place, and the first of these wins: an alias written in the source, this map, then the generated standard library table, then the guesses.
It combines with WithForceImportsPruning, which settles every path the map leaves out.
The map is read, not kept: pass the same map to as many concurrent calls as you like. Build it with github.com/go-openapi/codegen/formatting/resolve, which answers from the packages themselves rather than from the machine, so one map serves every build.
Example ¶
ExampleWithResolvedImports shows the map covering a package the promise gets wrong.
github.com/json-iterator/go declares jsoniter, and no rule reading the path says so.
package main
import (
"fmt"
"log"
"os"
"github.com/go-openapi/codegen/formatting"
)
func main() {
const src = `package p
import "github.com/json-iterator/go"
var _ = jsoniter.Marshal
`
show := func(label string, opts ...formatting.Option) {
fmt.Println(label)
if _, err := formatting.Format(os.Stdout, []byte(src), opts...); err != nil {
log.Fatal(err)
}
}
show("// the promise alone deletes an import the code uses",
formatting.WithForceImportsPruning())
show("// naming the package keeps it, and the promise still covers the rest",
formatting.WithForceImportsPruning(),
formatting.WithResolvedImports(map[string]string{
"github.com/json-iterator/go": "jsoniter",
}),
)
}
Output: // the promise alone deletes an import the code uses package p var _ = jsoniter.Marshal // naming the package keeps it, and the promise still covers the rest package p import "github.com/json-iterator/go" var _ = jsoniter.Marshal
func WithSimplifiedImportAliases ¶
func WithSimplifiedImportAliases() Option
WithSimplifiedImportAliases drops an alias that repeats the name its package declares.
import fmt "fmt" -> import "fmt" import strfmt "github.com/go-openapi/strfmt" -> import "github.com/go-openapi/strfmt"
A template that writes the alias even where Go would leave it out gets exact pruning without promising anything, because an alias states the name. This takes those aliases back out once the name is proven, so the output reads as ordinary Go. The second line above needs WithResolvedImports to name that package; the first is proven by the standard library table.
An alias survives when dropping it would lose something. jsoniter "github.com/json-iterator/go" keeps its alias even with the name proven, because the path does not say jsoniter and the bare import would leave nothing that does. So does an alias that renames a package, as sql "database/sql /driver", and so do _ and . imports.
Nothing is dropped on a guess. Without evidence from the table or the map, every alias stays.
Example ¶
ExampleWithSimplifiedImportAliases shows an alias written for safety being taken back out.
A template that aliases every import gets exact pruning with no option at all, because an alias states the name. This hands the reader ordinary Go once the name is proven.
package main
import (
"log"
"os"
"github.com/go-openapi/codegen/formatting"
)
func main() {
const src = `package p
import (
fmt "fmt"
strfmt "github.com/go-openapi/strfmt"
jsoniter "github.com/json-iterator/go"
)
var (
_ = fmt.Sprint
_ = strfmt.Date{}
_ = jsoniter.Marshal
)
`
if _, err := formatting.Format(os.Stdout, []byte(src),
formatting.WithSimplifiedImportAliases(),
formatting.WithResolvedImports(map[string]string{
"github.com/go-openapi/strfmt": "strfmt",
"github.com/json-iterator/go": "jsoniter",
}),
); err != nil {
log.Fatal(err)
}
// jsoniter keeps its alias although the name is proven: the path does not say jsoniter, so the
// bare import would leave nothing that does.
}
Output: package p import ( "fmt" "github.com/go-openapi/strfmt" jsoniter "github.com/json-iterator/go" ) var ( _ = fmt.Sprint _ = strfmt.Date{} _ = jsoniter.Marshal )
type Source ¶
Source lists the types Format accepts as source.
Both terms give up their bytes without copying them, so those are the only two. An io.Reader is deliberately absent: Format reads the source more than once — the parser retries a fragment as a declaration list and then as a statement list, a file whose imports may be shadowed is parsed a second time with scopes, and a fragment's original text is needed again at the end to restore the space around it — so a reader would be drained into a buffer at the door and the signature would promise a streaming that cannot happen.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
rules
Package rules holds the formatting rules an enable module registers.
|
Package rules holds the formatting rules an enable module registers. |
|
std
Package std answers which name a standard library import declares.
|
Package std answers which name a standard library import declares. |
|
Package resolve reads the name each imported package declares.
|
Package resolve reads the name each imported package declares. |