New AST check scans handler render functions that build site settings (getSiteSettings + doc["site_settings"]) and fails if they don't also set siteSettings["toolbar"], so admin-toolbar injection can't silently regress when a new public page type is added. - toolbar.go: AST scanner; exempts renderListingPage (auto SEO pages) and renderVersionPreview (carries its own preview banner). - check_toolbar.go: registration at Seq 280 (ID "28"). - registry_test.go: +1 expected check; golden fixtures regenerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
2.3 KiB
Go
97 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type toolbarViolation struct {
|
|
file string
|
|
line int
|
|
funcName string
|
|
}
|
|
|
|
// Functions that render full public pages but legitimately skip toolbar injection.
|
|
var toolbarExemptFuncs = map[string]bool{
|
|
// Auto-generated SEO listing pages — not block-editable.
|
|
"renderListingPage": true,
|
|
// Historical version preview — has its own preview banner; toolbar edit/publish
|
|
// buttons would be misleading in that context.
|
|
"renderVersionPreview": true,
|
|
}
|
|
|
|
// checkToolbarInjection scans handler Go files for public page render functions
|
|
// (identified by containing both getSiteSettings and doc["site_settings"]) and
|
|
// verifies each one also injects siteSettings["toolbar"] for the admin toolbar.
|
|
func checkToolbarInjection(root string) []toolbarViolation {
|
|
var violations []toolbarViolation
|
|
|
|
handlersDir := filepath.Join(root, "internal", "handlers")
|
|
if _, err := os.Stat(handlersDir); err != nil {
|
|
return nil
|
|
}
|
|
|
|
_ = filepath.Walk(handlersDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
srcText := string(src)
|
|
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, path, src, 0)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(root, path)
|
|
|
|
for _, decl := range f.Decls {
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fn.Body == nil {
|
|
continue
|
|
}
|
|
|
|
if toolbarExemptFuncs[fn.Name.Name] {
|
|
continue
|
|
}
|
|
|
|
bodyStart := fset.Position(fn.Body.Pos()).Offset
|
|
bodyEnd := fset.Position(fn.Body.End()).Offset
|
|
if bodyEnd > len(srcText) {
|
|
bodyEnd = len(srcText)
|
|
}
|
|
body := srcText[bodyStart:bodyEnd]
|
|
|
|
hasSiteSettings := strings.Contains(body, "getSiteSettings")
|
|
hasDocAssign := strings.Contains(body, `doc["site_settings"]`)
|
|
|
|
if hasSiteSettings && hasDocAssign {
|
|
hasToolbar := strings.Contains(body, `siteSettings["toolbar"]`)
|
|
if !hasToolbar {
|
|
violations = append(violations, toolbarViolation{
|
|
file: relPath,
|
|
line: fset.Position(fn.Pos()).Line,
|
|
funcName: fn.Name.Name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return violations
|
|
}
|