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
67 changes: 67 additions & 0 deletions .claude/rules/technical-writing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
paths:
- "**/*.go"
- "**/*.md"
---

# Technical writing style (go-openapi)

Applies to every committed comment, commit message, README and doc-site page.

The standard is Ernest Gowers, *Plain Words*: **be short, be simple, be human.**
His worked example is the whole rule:

DON'T Was this the realisation of an anticipated liability?
DO Did you expect to have to do this?

The abstract nouns carry no information; the concrete verb carries all of it.

## Two tests

**The grep test.** Does the sentence contain something a reader can search for — an
identifier, a file, a flag, an error, a number with a unit? Prose that names nothing has
described the code without pointing at it.

**The quotability test.** A sentence that would survive being quoted on its own is too
pleased with itself. Rewrite it until it merely sounds true.

## Never define by inversion

The worst and most frequent fault. A copula whose subject or predicate is a wh-clause
promises a definition and delivers a metaphor. Both directions are banned:

DON'T Coverage is what says which templates a suite never reaches.
DON'T What is lost is the doc comment.
DO Coverage records which templates the suite never executed.
DO A synthesized type loses its doc comment.

The rewrite is mechanical: find the verb hiding inside the wh-clause and make it the main
verb of the sentence.

`which is why` pointing back at a fact just stated is legitimate, and rationed — one per
comment is plenty.

## The rest

- **Name the thing.** `WithRoots`, not "the option that scopes a repository". Name the
error, the file, the flag, the upstream package, the constant.
- **Statement, not aphorism.** State mechanism and effect. Never close a paragraph on a
maxim: the reflex lands hardest on a closing sentence.
- **Keep a subject.** "New returns an error if the source is unreadable", not "What a
source leaves out is settled where it is declared".
- **Plain verbs.** add, fix, return, parse, reject, cap, prune, record. Code does not say,
judge, grant, refuse, know, mean to, or reach for. `report` is fine when something
genuinely reports.
- **Keep the numbers.** Sizes with units, counts, ratios, advisory ids. `286 -> 178 KiB`,
`GHSA-v2xp-g8xf-22pf`. Dropping them for a smoother sentence loses information.
- **Be human.** Address the reader where there is advice: "Use `WithRoot` to confine local
loading." Admit the awkward thing rather than smoothing it over.

## Self-check

# definition by inversion, both directions
grep -rnE '\b(is|are) (what|where) [a-z]' --include='*.go' --include='*.md' .
grep -rnE '(^|\. )What [a-z][a-z ,-]{3,50} (is|are) ' --include='*.go' --include='*.md' .

Subtract the legitimate `which/that/this/it is what` before judging the first one.
Neither grep is a verdict — they find one fault out of six. The others need reading.
10 changes: 6 additions & 4 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ updates:
# - golang.org (e.g. golang.org/x/... packages)
# - other dependencies (direct or indirect)
#
# * All groups are checked once a week and each produce at most 1 PR.
# * All groups are checked twice a week and each produce at most 1 PR.
# * All dependabot PRs are auto-approved
#
# Auto-merging policy, when requirements are met:
Expand All @@ -34,9 +34,10 @@ updates:
directories:
- "**/*"
schedule:
interval: "weekly"
day: "friday"
open-pull-requests-limit: 4
interval: "cron"
cronjob: "35 7 * * 3,6" # => update twice a week
timezone: CEST
open-pull-requests-limit: 16 # => {# modules} x {# groups}
groups:
development-dependencies:
patterns:
Expand All @@ -53,6 +54,7 @@ updates:
exclude-patterns:
- "github.com/go-openapi/testify"

# not auto-merge: require review and manual merge action
other-dependencies:
exclude-patterns:
- "github.com/go-openapi/*"
Expand Down
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ linters:
- errchkjson
- errorlint
- exhaustruct
- exhaustruct_v5
- forcetypeassert
- funlen
- gochecknoglobals
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Badges: release & docker images -->
<!-- Badges: code quality -->
<!-- Badges: license & compliance -->
[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url]
[![Release][release-badge]][release-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url]
<!-- Badges: documentation & support -->
<!-- Badges: others & stats -->
[![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge]
Expand Down Expand Up @@ -101,13 +101,9 @@ Maintainers can cut a new release by running:
[gomod-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Fcodegen.svg
[gomod-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Fcodegen
<!-- Badges: code quality -->
[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/codegen
[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/codegen
[codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/codegen
[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/codegen
<!-- Badges: documentation & support -->
[doc-badge]: https://img.shields.io/badge/doc-site-blue?link=https%3A%2F%2Fgoswagger.io%2Fgo-openapi%2F
[doc-url]: https://goswagger.io/go-openapi
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/codegen
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/codegen
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
Expand Down
8 changes: 8 additions & 0 deletions funcmaps/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package funcmaps exposes utilities to work with [template.FuncMap].
//
// * funcmap merging, with guards against unwary overrides (coalesce, protecting builtins)
// * a default funcmap for golang codegen, with common mangling for go identifiers, go comments handling etc
package funcmaps
165 changes: 165 additions & 0 deletions funcmaps/golang/comments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package golang

import (
"fmt"
"reflect"
"strings"
)

// lineComment renders its arguments as a complete Go line-comment block, emitting
// the "//" markers itself so call sites only pass content.
//
// Arguments are stringified and concatenated with fmt.Sprint semantics (a space is
// inserted only between two non-string operands); nil arguments are skipped, so
// optional template values drop out cleanly. This lets a composed comment be built
// inline, e.g. {{ lineComment "MinProperties: " .MinProperties }}.
//
// Besides factoring out comment construction, it harmonizes the output: every line
// is prefixed with "// " (a single space), CR/CRLF are normalized, and per-line
// trailing whitespace is trimmed.
//
// Blank lines are preserved (they matter to godoc and to codescan).
//
// A blank line left trailing the whole comment block is dropped by the Go formatter.
//
// Splitting on embedded newlines keeps multi-line content fully commented so it cannot break out of the comment,
// and the guaranteed space after "//" keeps content from accidentally (or maliciously)
// forming a compiler directive such as //go:embed or //line.
//
// Blank input yields no output.
func lineComment(args ...any) string {
return renderLineComment("// ", args)
}

// linePadComment renders its arguments as a Go line-comment block like [lineComment],
// but indents every rendered line by pad after the "//" marker,
// including the continuation lines produced by a multi-line argument.
//
// This preserves a fixed indentation across wrapped lines, as required by the
// indentation-significant swagger:meta package doc block.
//
// pad is the indentation that follows "//": a pad of " " yields "// text". A
// leading space is inserted when pad does not already start with whitespace, so
// the marker can never accidentally (or maliciously) form a compiler directive.
// Empty content yields no output.
func linePadComment(pad string, args ...any) string {
if pad == "" || (pad[0] != ' ' && pad[0] != '\t') {
pad = " " + pad
}

return renderLineComment("//"+pad, args)
}

// wrapBlockComment renders text as a complete Go block comment.
//
// It emits the "/*" and "*/" markers itself so call sites only pass the text.
//
// It neutralizes any inner "*/" so spec text cannot terminate the comment early,
// normalizes newlines and trims trailing whitespace. Single-line text stays
// inline ("/* text */"); multi-line text is wrapped on its own lines.
//
// Empty input yields no output.
func wrapBlockComment(str string) string {
str = strings.TrimRight(normalizeNewlines(str), " \t\n")
if str == "" {
return ""
}

str = strings.ReplaceAll(str, "*/", "[*]/")

if strings.ContainsRune(str, '\n') {
return "/*\n" + str + "\n*/"
}

return "/* " + str + " */"
}

// renderLineComment is the shared core of [lineComment] and [linePadComment].
//
// It stringifies args (fmt.Sprint semantics, nil arguments skipped), normalizes CR/CRLF,
// trims trailing whitespace from each line, then prefixes every line with marker (blank lines become a bare "//").
//
// Splitting on embedded newlines keeps multi-line content fully commented so it cannot break out of the comment.
//
// Blank lines are preserved, including a blank line trailing the content:
// they carry meaning for godoc paragraphs and for codescan.
//
// A blank line that ends up trailing the whole comment block is left to the Go formatter to drop:
// renderLineComment may be called several times and it cannot determine which trailing blank line will be the
// last block to trim.
//
// Content that is entirely blank yields no output.
func renderLineComment(marker string, args []any) string {
kept := make([]any, 0, len(args))
for _, arg := range args {
if deref, ok := derefArg(arg); ok {
kept = append(kept, deref)
}
}

str := normalizeNewlines(fmt.Sprint(kept...))
if strings.TrimSpace(str) == "" {
return ""
}

lines := strings.Split(str, "\n")
for i, line := range lines {
line = strings.TrimRight(line, " \t")
if line == "" {
lines[i] = "//"

continue
}

lines[i] = marker + line
}

return strings.Join(lines, "\n")
}

// derefArg unwraps pointer arguments so they stringify by value, mirroring how
// text/template prints a pointer field with {{ .X }}.
//
// A nil interface or a nil pointer is reported as absent (ok=false) so the caller can skip it.
// This keeps composed comments such as {{ lineComment "MinProperties: " .MinProperties }}
// printing the int64 value rather than the *int64 address.
func derefArg(arg any) (any, bool) {
const limit = 1000 // guard against malicious overflow

if arg == nil {
return nil, false
}

v := reflect.ValueOf(arg)
for i := 0; v.Kind() == reflect.Pointer; i++ {
if i > limit {
return nil, false
}

if v.IsNil() {
return nil, false
}

v = v.Elem()
}

if v.CanInterface() {
return v.Interface(), true
}

return nil, false
}

// normalizeNewlines rewrites CRLF and lone CR to LF so comment helpers can split reliably on "\n".
func normalizeNewlines(str string) string {
if !strings.ContainsRune(str, '\r') {
return str
}

str = strings.ReplaceAll(str, "\r\n", "\n")

return strings.ReplaceAll(str, "\r", "\n")
}
2 changes: 1 addition & 1 deletion funcmaps/golang/comments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestWrapBlockComment(t *testing.T) {
// TestCommentHelpersRegistered ensures the helpers are wired into the funcmap
// under their template-facing names.
func TestCommentHelpersRegistered(t *testing.T) {
fm := testMap()
fm := testGoMap()
assert.Contains(t, fm, "lineComment")
assert.Contains(t, fm, "linePadComment")
assert.Contains(t, fm, "blockComment")
Expand Down
5 changes: 5 additions & 0 deletions funcmaps/golang/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

// Package golang provides the Go-specific template function map used by code generators.
package golang
Loading
Loading