diff --git a/.github/workflows/diff.yaml b/.github/workflows/diff.yaml new file mode 100644 index 0000000..8459ec2 --- /dev/null +++ b/.github/workflows/diff.yaml @@ -0,0 +1,32 @@ +name: check diff + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + check-latest: true + + - name: Generate test + run: | + make generate + + - name: Check diff + run: | + git diff --exit-code diff --git a/analyzer.go b/analyzer.go index 8b244d8..c05f784 100644 --- a/analyzer.go +++ b/analyzer.go @@ -34,6 +34,8 @@ func run(pass *analysis.Pass) (any, error) { return nil, fmt.Errorf("want %T, got %T", spctor, pass.ResultOf[inspect.Analyzer]) } + wellKnownHeaders := initialism() + nodeFilter := []ast.Node{ (*ast.CallExpr)(nil), } @@ -97,8 +99,8 @@ func run(pass *analysis.Pass) (any, error) { return } - headerKeyCanonical := http.CanonicalHeaderKey(headerKeyOriginal) - if headerKeyOriginal == headerKeyCanonical { + headerKeyCanonical, isWellKnown := canonicalHeaderKey(headerKeyOriginal, wellKnownHeaders) + if headerKeyOriginal == headerKeyCanonical || isWellKnown { return } @@ -131,6 +133,17 @@ func run(pass *analysis.Pass) (any, error) { return nil, outerErr } +func canonicalHeaderKey(s string, m map[string]string) (string, bool) { + canonical := http.CanonicalHeaderKey(s) + + wellKnown, ok := m[canonical] + if !ok { + return canonical, ok + } + + return wellKnown, ok +} + func isHTTPHeader(named *types.Named) bool { return named.Obj() != nil && named.Obj().Pkg() != nil && diff --git a/analyzer_test.go b/analyzer_test.go index b9a78ff..56a8153 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -22,6 +22,7 @@ func TestAnalyzer(t *testing.T) { "common", "embedded", "global", + "initialism", "struct", } diff --git a/cmd/initialismer/main.go b/cmd/initialismer/main.go new file mode 100644 index 0000000..2441730 --- /dev/null +++ b/cmd/initialismer/main.go @@ -0,0 +1,390 @@ +package main + +import ( + "cmp" + "flag" + "fmt" + "html/template" + "log/slog" + "net/http" + "os" + "slices" +) + +var mustBeIgnore = [...]string{ + "A-IM", + "Accept", + "Accept-Additions", + "Accept-CH", + "Accept-Charset", + "Accept-Datetime", + "Accept-Encoding", + "Accept-Features", + "Accept-Language", + "Accept-Patch", + "Accept-Post", + "Accept-Ranges", + "Accept-Signature", + "Access-Control", + "Access-Control-Allow-Credentials", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Expose-Headers", + "Access-Control-Max-Age", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Age", + "Allow", + "ALPN", + "Alt-Svc", + "Alt-Used", + "Alternates", + "AMP-Cache-Transform", + "Apply-To-Redirect-Ref", + "Authentication-Control", + "Authentication-Info", + "Authorization", + "C-Ext", + "C-Man", + "C-Opt", + "C-PEP", + "C-PEP-Info", + "Cache-Control", + "Cache-Status", + "Cal-Managed-ID", + "CalDAV-Timezones", + "Capsule-Protocol", + "CDN-Cache-Control", + "CDN-Loop", + "Cert-Not-After", + "Cert-Not-Before", + "Clear-Site-Data", + "Client-Cert", + "Client-Cert-Chain", + "Close", + "Configuration-Context", + "Connection", + "Content-Base", + "Content-Digest", + "Content-Disposition", + "Content-Encoding", + "Content-ID", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-MD5", + "Content-Range", + "Content-Script-Type", + "Content-Security-Policy", + "Content-Security-Policy-Report-Only", + "Content-Style-Type", + "Content-Type", + "Content-Version", + "Cookie", + "Cookie2", + "Cross-Origin-Embedder-Policy", + "Cross-Origin-Embedder-Policy-Report-Only", + "Cross-Origin-Opener-Policy", + "Cross-Origin-Opener-Policy-Report-Only", + "Cross-Origin-Resource-Policy", + "DASL", + "Date", + "DAV", + "Default-Style", + "Delta-Base", + "Depth", + "Derived-From", + "Destination", + "Differential-ID", + "Digest", + "DPoP", + "DPoP-Nonce", + "Early-Data", + "EDIINT-Features", + "Expect", + "Expect-CT", + "X-Correlation-ID", + "X-UA-Compatible", + "X-XSS-Protection", + "Expires", + "Ext", + "Forwarded", + "From", + "GetProfile", + "Hobareg", + "Host", + "HTTP2-Settings", + "If", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Schedule-Tag-Match", + "If-Unmodified-Since", + "IM", + "Include-Referred-Token-Binding-ID", + "Isolation", + "Keep-Alive", + "Label", + "Last-Event-ID", + "Last-Modified", + "Link", + "Location", + "Lock-Token", + "Man", + "Max-Forwards", + "Memento-Datetime", + "Meter", + "Method-Check", + "Method-Check-Expires", + "MIME-Version", + "Negotiate", + "NEL", + "OData-EntityId", + "OData-Isolation", + "OData-MaxVersion", + "OData-Version", + "Opt", + "Optional-WWW-Authenticate", + "Ordering-Type", + "Origin", + "Origin-Agent-Cluster", + "OSCORE", + "OSLC-Core-Version", + "Overwrite", + "P3P", + "PEP", + "PEP-Info", + "Permissions-Policy", + "PICS-Label", + "Ping-From", + "Ping-To", + "Position", + "Pragma", + "Prefer", + "Preference-Applied", + "Priority", + "ProfileObject", + "Protocol", + "Protocol-Info", + "Protocol-Query", + "Protocol-Request", + "Proxy-Authenticate", + "Proxy-Authentication-Info", + "Proxy-Authorization", + "Proxy-Features", + "Proxy-Instruction", + "Proxy-Status", + "Public", + "Public-Key-Pins", + "Public-Key-Pins-Report-Only", + "Range", + "Redirect-Ref", + "Referer", + "Referer-Root", + "Refresh", + "Repeatability-Client-ID", + "Repeatability-First-Sent", + "Repeatability-Request-ID", + "Repeatability-Result", + "Replay-Nonce", + "Reporting-Endpoints", + "Repr-Digest", + "Retry-After", + "Safe", + "Schedule-Reply", + "Schedule-Tag", + "Sec-GPC", + "Sec-Purpose", + "Sec-Token-Binding", + "Sec-WebSocket-Accept", + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Version", + "Security-Scheme", + "Server", + "Server-Timing", + "Set-Cookie", + "Set-Cookie2", + "SetProfile", + "Signature", + "Signature-Input", + "SLUG", + "SoapAction", + "Status-URI", + "Strict-Transport-Security", + "Sunset", + "Surrogate-Capability", + "Surrogate-Control", + "TCN", + "TE", + "Timeout", + "Timing-Allow-Origin", + "Topic", + "Traceparent", + "Tracestate", + "Trailer", + "Transfer-Encoding", + "TTL", + "Upgrade", + "Urgency", + "URI", + "User-Agent", + "Variant-Vary", + "Vary", + "Via", + "Want-Content-Digest", + "Want-Digest", + "Want-Repr-Digest", + "Warning", + "X-Content-Type-Options", + "X-Frame-Options", + "ETag", + "DNT", + "X-Request-ID", + "X-XSS", + "X-DNS-Prefetch-Control", + "WWW-Authenticate", + "X-WebKit-CSP", + "X-Real-IP", +} + +type generateTarget uint8 + +func (t generateTarget) String() string { + switch t { + case generateTest: + return "test" + case generateTestGolden: + return "test-golden" + case generateMapping: + return "mapping" + default: + return "unknown" + } +} + +const ( + generateUnknown generateTarget = iota + generateTest + generateTestGolden + generateMapping +) + +func parseTarget() (generateTarget, error) { + var t string + flag.StringVar(&t, "target", "", "test or mapping") + flag.Parse() + + switch t { + case generateTest.String(): + return generateTest, nil + + case generateMapping.String(): + return generateMapping, nil + + case generateTestGolden.String(): + return generateTestGolden, nil + + default: + return generateUnknown, fmt.Errorf("unknown target %q", t) + } +} + +func main() { + filtered := make(map[string]struct{}, len(mustBeIgnore)) + + type ignore struct { + Canonical, + Original string + } + + results := make([]ignore, 0, len(mustBeIgnore)) + + for _, s := range mustBeIgnore { + _, ok := filtered[s] + if ok { + slog.Error("has duplicate:", slog.String("duplicate", s)) + os.Exit(1) + } + filtered[s] = struct{}{} + + canonical := http.CanonicalHeaderKey(s) + if canonical != s { + results = append(results, ignore{ + Canonical: canonical, + Original: s, + }) + } + } + slices.SortFunc(results, func(a, b ignore) int { + return cmp.Compare(a.Canonical, b.Canonical) + }) + + genTarget, err := parseTarget() + if err != nil { + slog.Error("parse target:", slog.Any("error", err)) + } + + var tmpl string + switch genTarget { + case generateTest: + tmpl = tmplTest + + case generateMapping: + tmpl = tmplMapping + + case generateTestGolden: + tmpl = tmplTestGolden + } + + t := template.Must(template.New("").Parse(tmpl)) + + err = t.Execute(os.Stdout, results) + if err == nil { + return + } + + slog.Error("execute template:", slog.Any("error", err)) + os.Exit(1) +} + +const tmplMapping = `// Code generated by initialismer; DO NOT EDIT. +package canonicalheader + +// initialism mapping of not canonical headers from +// https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +// https://www.iana.org/assignments/http-fields/http-fields.xhtml. +func initialism() map[string]string { + return map[string]string{ + {{range .}}"{{.Canonical}}": "{{.Original}}", + {{end}} + } +} +` + +const tmplTest = `// Code generated by initialismer; DO NOT EDIT. +package initialism + +import "net/http" + +func _() { + h := http.Header{} + {{range .}} + h.Get("{{.Original}}"){{end}} +} +` + +const tmplTestGolden = `// Code generated by initialismer; DO NOT EDIT. +package initialism + +import "net/http" + +func _() { + h := http.Header{} + {{range .}} + h.Get("{{.Original}}"){{end}} +} +` diff --git a/initialism.go b/initialism.go new file mode 100644 index 0000000..c3d91c2 --- /dev/null +++ b/initialism.go @@ -0,0 +1,75 @@ +// Code generated by initialismer; DO NOT EDIT. +package canonicalheader + +// initialism mapping of not canonical headers from +// https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +// https://www.iana.org/assignments/http-fields/http-fields.xhtml. +func initialism() map[string]string { + return map[string]string{ + "A-Im": "A-IM", + "Accept-Ch": "Accept-CH", + "Alpn": "ALPN", + "Amp-Cache-Transform": "AMP-Cache-Transform", + "C-Pep": "C-PEP", + "C-Pep-Info": "C-PEP-Info", + "Cal-Managed-Id": "Cal-Managed-ID", + "Caldav-Timezones": "CalDAV-Timezones", + "Cdn-Cache-Control": "CDN-Cache-Control", + "Cdn-Loop": "CDN-Loop", + "Content-Id": "Content-ID", + "Content-Md5": "Content-MD5", + "Dasl": "DASL", + "Dav": "DAV", + "Differential-Id": "Differential-ID", + "Dnt": "DNT", + "Dpop": "DPoP", + "Dpop-Nonce": "DPoP-Nonce", + "Ediint-Features": "EDIINT-Features", + "Etag": "ETag", + "Expect-Ct": "Expect-CT", + "Getprofile": "GetProfile", + "Http2-Settings": "HTTP2-Settings", + "Im": "IM", + "Include-Referred-Token-Binding-Id": "Include-Referred-Token-Binding-ID", + "Last-Event-Id": "Last-Event-ID", + "Mime-Version": "MIME-Version", + "Nel": "NEL", + "Odata-Entityid": "OData-EntityId", + "Odata-Isolation": "OData-Isolation", + "Odata-Maxversion": "OData-MaxVersion", + "Odata-Version": "OData-Version", + "Optional-Www-Authenticate": "Optional-WWW-Authenticate", + "Oscore": "OSCORE", + "Oslc-Core-Version": "OSLC-Core-Version", + "P3p": "P3P", + "Pep": "PEP", + "Pep-Info": "PEP-Info", + "Pics-Label": "PICS-Label", + "Profileobject": "ProfileObject", + "Repeatability-Client-Id": "Repeatability-Client-ID", + "Repeatability-Request-Id": "Repeatability-Request-ID", + "Sec-Gpc": "Sec-GPC", + "Sec-Websocket-Accept": "Sec-WebSocket-Accept", + "Sec-Websocket-Extensions": "Sec-WebSocket-Extensions", + "Sec-Websocket-Key": "Sec-WebSocket-Key", + "Sec-Websocket-Protocol": "Sec-WebSocket-Protocol", + "Sec-Websocket-Version": "Sec-WebSocket-Version", + "Setprofile": "SetProfile", + "Slug": "SLUG", + "Soapaction": "SoapAction", + "Status-Uri": "Status-URI", + "Tcn": "TCN", + "Te": "TE", + "Ttl": "TTL", + "Uri": "URI", + "Www-Authenticate": "WWW-Authenticate", + "X-Correlation-Id": "X-Correlation-ID", + "X-Dns-Prefetch-Control": "X-DNS-Prefetch-Control", + "X-Real-Ip": "X-Real-IP", + "X-Request-Id": "X-Request-ID", + "X-Ua-Compatible": "X-UA-Compatible", + "X-Webkit-Csp": "X-WebKit-CSP", + "X-Xss": "X-XSS", + "X-Xss-Protection": "X-XSS-Protection", + } +} diff --git a/makefile b/makefile index 2b91254..a96cb62 100644 --- a/makefile +++ b/makefile @@ -5,3 +5,8 @@ test: linter: golangci-lint -v run ./... + +generate: + go run ./cmd/initialismer/*.go -target="mapping" > ./initialism.go + go run ./cmd/initialismer/*.go -target="test" > ./testdata/src/initialism/initialism.go + gofmt -w ./initialism.go ./testdata/src/initialism/initialism.go diff --git a/testdata/src/initialism/initialism.go b/testdata/src/initialism/initialism.go new file mode 100644 index 0000000..a4cccc1 --- /dev/null +++ b/testdata/src/initialism/initialism.go @@ -0,0 +1,74 @@ +// Code generated by initialismer; DO NOT EDIT. +package initialism + +import "net/http" + +func _() { + h := http.Header{} + + h.Get("A-IM") + h.Get("Accept-CH") + h.Get("ALPN") + h.Get("AMP-Cache-Transform") + h.Get("C-PEP") + h.Get("C-PEP-Info") + h.Get("Cal-Managed-ID") + h.Get("CalDAV-Timezones") + h.Get("CDN-Cache-Control") + h.Get("CDN-Loop") + h.Get("Content-ID") + h.Get("Content-MD5") + h.Get("DASL") + h.Get("DAV") + h.Get("Differential-ID") + h.Get("DNT") + h.Get("DPoP") + h.Get("DPoP-Nonce") + h.Get("EDIINT-Features") + h.Get("ETag") + h.Get("Expect-CT") + h.Get("GetProfile") + h.Get("HTTP2-Settings") + h.Get("IM") + h.Get("Include-Referred-Token-Binding-ID") + h.Get("Last-Event-ID") + h.Get("MIME-Version") + h.Get("NEL") + h.Get("OData-EntityId") + h.Get("OData-Isolation") + h.Get("OData-MaxVersion") + h.Get("OData-Version") + h.Get("Optional-WWW-Authenticate") + h.Get("OSCORE") + h.Get("OSLC-Core-Version") + h.Get("P3P") + h.Get("PEP") + h.Get("PEP-Info") + h.Get("PICS-Label") + h.Get("ProfileObject") + h.Get("Repeatability-Client-ID") + h.Get("Repeatability-Request-ID") + h.Get("Sec-GPC") + h.Get("Sec-WebSocket-Accept") + h.Get("Sec-WebSocket-Extensions") + h.Get("Sec-WebSocket-Key") + h.Get("Sec-WebSocket-Protocol") + h.Get("Sec-WebSocket-Version") + h.Get("SetProfile") + h.Get("SLUG") + h.Get("SoapAction") + h.Get("Status-URI") + h.Get("TCN") + h.Get("TE") + h.Get("TTL") + h.Get("URI") + h.Get("WWW-Authenticate") + h.Get("X-Correlation-ID") + h.Get("X-DNS-Prefetch-Control") + h.Get("X-Real-IP") + h.Get("X-Request-ID") + h.Get("X-UA-Compatible") + h.Get("X-WebKit-CSP") + h.Get("X-XSS") + h.Get("X-XSS-Protection") +}