Files
go-telegram/cmd/audit/main.go
T
lukaszraczylo ac7cae8fa7 Initial release of go-telegram
A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
2026-05-09 13:09:27 +01:00

292 lines
7.5 KiB
Go

// Command audit reports IR-level codegen fallbacks and signature drift.
//
// Usage:
//
// audit -ir <path> (default internal/spec/api.json)
// audit -overrides <path> (default internal/spec/overrides.json)
// audit -drift (compare against -against ref's IR; off by default)
// audit -against <ref> (git ref to diff drift against; default HEAD~1)
//
// Exit codes:
//
// 0 — clean
// 1 — unaccounted bool fallbacks or any-typed fields
// 2 — drift detected (signature changed)
// 3 — invalid IR or overrides
package main
import (
"flag"
"fmt"
"github.com/goccy/go-json"
"os"
"os/exec"
"strings"
"github.com/lukaszraczylo/go-telegram/internal/spec"
)
const (
exitClean = 0
exitFallback = 1
exitDrift = 2
exitInvalid = 3
)
func main() {
irPath := flag.String("ir", "internal/spec/api.json", "path to IR JSON")
ovPath := flag.String("overrides", "internal/spec/overrides.json", "path to overrides JSON")
checkDrift := flag.Bool("drift", false, "compare against -against ref's IR for signature changes")
againstRef := flag.String("against", "HEAD~1", "git ref to diff drift against (e.g. origin/main, HEAD~1)")
flag.Parse()
api, err := loadIR(*irPath)
if err != nil {
fmt.Fprintln(os.Stderr, "audit:", err)
os.Exit(exitInvalid)
}
overrides, err := spec.LoadOverrides(*ovPath)
if err != nil {
fmt.Fprintln(os.Stderr, "audit:", err)
os.Exit(exitInvalid)
}
var problems []string
problems = append(problems, auditBool(api, overrides)...)
problems = append(problems, auditAny(api)...)
driftFound := false
if *checkDrift {
if d, err := auditDrift(*irPath, *againstRef, api); err != nil {
fmt.Fprintln(os.Stderr, "audit: drift check skipped:", err)
} else if len(d) > 0 {
fmt.Println("Drift detected (signatures changed since HEAD):")
for _, p := range d {
fmt.Println(" ", p)
}
driftFound = true
}
}
if len(problems) == 0 && !driftFound {
fmt.Println("audit: clean")
os.Exit(exitClean)
}
if len(problems) > 0 {
fmt.Println("Codegen fallbacks requiring action:")
for _, p := range problems {
fmt.Println(" ", p)
}
fmt.Println()
fmt.Println("To resolve: extend cmd/scrape/method.go regex patterns OR")
fmt.Println("add an entry to internal/spec/overrides.json.")
os.Exit(exitFallback)
}
// drift only, no fallbacks
os.Exit(exitDrift)
}
func loadIR(path string) (*spec.API, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open IR: %w", err)
}
defer func() { _ = f.Close() }()
var api spec.API
if err := json.NewDecoder(f).Decode(&api); err != nil {
return nil, fmt.Errorf("decode IR: %w", err)
}
return &api, nil
}
// auditBool returns problems for methods returning bool whose docs don't
// actually say "Returns True" / etc. and which aren't in the approved list.
func auditBool(api *spec.API, ov *spec.Overrides) []string {
var out []string
for _, m := range api.Methods {
if m.Returns.Kind != spec.KindPrimitive || m.Returns.Name != "bool" {
continue
}
if ov.IsBoolApproved(m.Name) {
continue
}
if looksGenuinelyBool(m.Doc) {
continue
}
snippet := m.Doc
if len(snippet) > 120 {
snippet = snippet[:120] + "…"
}
out = append(out, fmt.Sprintf("bool fallback: %s — doc: %q", m.Name, snippet))
}
return out
}
func looksGenuinelyBool(doc string) bool {
for _, p := range []string{
"Returns True", "Returns true",
"True is returned", "true is returned",
"Returns Boolean", "Returns Bool",
} {
if strings.Contains(doc, p) {
return true
}
}
return false
}
// auditAny scans the IR for any KindOneOf TypeRef that would render as
// `any` in generated code (not matched by ChatID/InputFile-or-string/known
// union heuristics). Reports each occurrence with location.
func auditAny(api *spec.API) []string {
var out []string
isKnownUnion := func(variants []string) bool {
if hasVariants(variants, "int64", "string") {
return true // ChatID
}
if hasVariants(variants, "InputFile", "string") {
return true // *InputFile
}
// ReplyMarkup union: all four keyboard types — emitter renders as `any` intentionally
if hasVariants(variants, "InlineKeyboardMarkup", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ForceReply") {
return true
}
for _, t := range api.Types {
if len(t.OneOf) > 0 && sameSet(variants, t.OneOf) {
return true
}
}
return false
}
isAny := func(tr spec.TypeRef) bool {
return tr.Kind == spec.KindOneOf && !isKnownUnion(tr.Variants)
}
for _, t := range api.Types {
for _, f := range t.Fields {
if isAny(f.Type) {
out = append(out, fmt.Sprintf("any field: %s.%s (variants=%v)", t.Name, f.Name, f.Type.Variants))
}
}
}
for _, m := range api.Methods {
if isAny(m.Returns) {
out = append(out, fmt.Sprintf("any return: %s (variants=%v)", m.Name, m.Returns.Variants))
}
for _, p := range m.Params {
if isAny(p.Type) {
out = append(out, fmt.Sprintf("any param: %s.%s (variants=%v)", m.Name, p.Name, p.Type.Variants))
}
}
}
return out
}
func hasVariants(got []string, want ...string) bool {
if len(got) != len(want) {
return false
}
seen := map[string]int{}
for _, g := range got {
seen[g]++
}
for _, w := range want {
seen[w]--
}
for _, v := range seen {
if v != 0 {
return false
}
}
return true
}
func sameSet(a, b []string) bool {
if len(a) != len(b) {
return false
}
return hasVariants(a, b...)
}
// auditDrift compares method/return signatures between the given git ref's
// version of irPath and the in-memory current IR. Returns a list of
// human-readable change descriptions.
func auditDrift(irPath, againstRef string, current *spec.API) ([]string, error) {
cmd := exec.Command("git", "show", againstRef+":"+irPath) // #nosec G204 - operator tool, ref controlled by caller
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git show %s: %w", againstRef, err)
}
var prev spec.API
if err := json.Unmarshal(out, &prev); err != nil {
return nil, fmt.Errorf("decode %s IR: %w", againstRef, err)
}
return diffSignatures(&prev, current), nil
}
func diffSignatures(prev, cur *spec.API) []string {
var changes []string
pmeth := indexByName(prev.Methods, func(m spec.MethodDecl) string { return m.Name })
cmeth := indexByName(cur.Methods, func(m spec.MethodDecl) string { return m.Name })
for name, p := range pmeth {
c, ok := cmeth[name]
if !ok {
changes = append(changes, fmt.Sprintf("removed method: %s", name))
continue
}
if !typeRefEqual(p.Returns, c.Returns) {
changes = append(changes, fmt.Sprintf(
"method %s return changed: %s → %s",
name, formatTypeRef(p.Returns), formatTypeRef(c.Returns)))
}
}
for name := range cmeth {
if _, ok := pmeth[name]; !ok {
changes = append(changes, fmt.Sprintf("added method: %s", name))
}
}
return changes
}
func indexByName[T any](xs []T, f func(T) string) map[string]T {
out := map[string]T{}
for _, x := range xs {
out[f(x)] = x
}
return out
}
func typeRefEqual(a, b spec.TypeRef) bool {
if a.Kind != b.Kind || a.Name != b.Name {
return false
}
if (a.ElemType == nil) != (b.ElemType == nil) {
return false
}
if a.ElemType != nil && !typeRefEqual(*a.ElemType, *b.ElemType) {
return false
}
return sameSet(a.Variants, b.Variants)
}
func formatTypeRef(t spec.TypeRef) string {
switch t.Kind {
case spec.KindPrimitive:
return t.Name
case spec.KindNamed:
return t.Name
case spec.KindArray:
if t.ElemType != nil {
return "[]" + formatTypeRef(*t.ElemType)
}
return "[]any"
case spec.KindOneOf:
return "(" + strings.Join(t.Variants, " | ") + ")"
}
return "?"
}