feat(kiosk): resolve constant plugin IDs, overrides and the CheckT helper

- kiosk.ReadIdentity: ID()/Requires() as literals or constants across packages and modules, read with go/parser, never run
- plugin minimums from go.mod requires; v0.0.0 is an error, a pseudo-version warns
- kiosk.Validate, kiosk.CheckT (no testing import)
- plugin:add reads IDs through kiosk.ReadIdentity, so const-ID plugins register
This commit is contained in:
Jakub Zych
2026-10-29 17:02:38 +01:00
parent ec6a1955fc
commit 51ec2f9daa
22 changed files with 1156 additions and 115 deletions

View File

@@ -7,13 +7,10 @@ import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/yuin/goldmark"
@@ -97,17 +94,19 @@ func Derive(ctx context.Context, dir string, opts DeriveOptions) (Manifest, []st
}
m.Framework = fw
ident, err := readLiteralIdentity(abs)
r := newResolver(ctx, abs)
r.modLoaded, r.root, r.mod = true, root, mod
ident, err := r.identity(abs)
if err != nil {
return Manifest{}, nil, err
}
m.ID = ident.ID
if len(ident.Requires) > 0 {
m.Requires = Requires{}
for _, id := range ident.Requires {
m.Requires[id] = ""
}
reqs, w, err := r.minimums(ident.Requires)
warnings = append(warnings, w...)
if err != nil {
return Manifest{}, warnings, err
}
m.Requires = reqs
existing, err := readExisting(abs)
if err != nil {
@@ -289,73 +288,6 @@ func plainText(n gast.Node, src []byte) string {
return strings.Join(strings.Fields(b.String()), " ")
}
// identity is what Task 2's reader returns: the literals of ID() and
// Requires().
type identity struct {
ID string
Requires []string
}
// readLiteralIdentity reads the string literals returned by ID() and
// Requires() in dir/plugin.go.
func readLiteralIdentity(dir string) (identity, error) {
path := filepath.Join(dir, "plugin.go")
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return identity{}, fmt.Errorf("kiosk: parse %s: %w", path, err)
}
var id identity
var idErr, reqErr error
foundID := false
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Recv == nil || fn.Body == nil {
continue
}
ret := lastReturn(fn)
switch fn.Name.Name {
case "ID":
foundID = true
lit, ok := ret.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
idErr = fmt.Errorf("kiosk: %s: ID() must return a string literal", path)
continue
}
id.ID, _ = strconv.Unquote(lit.Value)
case "Requires":
switch e := ret.(type) {
case *ast.Ident:
if e.Name != "nil" {
reqErr = fmt.Errorf("kiosk: %s: Requires() must return nil or a []string literal", path)
}
case *ast.CompositeLit:
for _, el := range e.Elts {
lit, ok := el.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
reqErr = fmt.Errorf("kiosk: %s: Requires() must return string literals", path)
break
}
s, _ := strconv.Unquote(lit.Value)
id.Requires = append(id.Requires, s)
}
default:
reqErr = fmt.Errorf("kiosk: %s: Requires() must return nil or a []string literal", path)
}
}
}
if !foundID {
return identity{}, fmt.Errorf("kiosk: %s has no ID() method", path)
}
if idErr != nil {
return identity{}, idErr
}
if reqErr != nil {
return identity{}, reqErr
}
return id, nil
}
// lastReturn is the single result of fn's last top-level return, or nil.
func lastReturn(fn *ast.FuncDecl) ast.Expr {
var res ast.Expr