mox/vendor/github.com/mjl-/sherpadoc/check.go
Mechiel Lukkien a2c9cfc55b
webadmin: don't show runtime typecheck error for invalid values in dmarc and tls reports
several fields in dmarc and tls reports have known string values. we have a Go
string type for them. sherpats (through sherpadoc) turns those strings into
typescript enums, and sherpats generates runtime-typechecking code (to enforce
correct types for incoming json, to prevent failing deeper in the code when we
get invalid data (much harder to debug)). the Go not-really-enum types allow
other values, and real-world reports have unknown/unspecified/invalid values.
this uses the sherpadoc -rename flag to turn those enums into regular untyped
strings, so sherpats doesn't generate enum-enforcing runtime type checking
code.

this required an update to sherpadoc, to properly handling renaming a type to a
basic type instead of another named type.

for issue #161 by RobSlgm, thanks for reporting!
2024-05-09 15:58:14 +02:00

178 lines
4.8 KiB
Go

package sherpadoc
import (
"fmt"
)
// IsBasicType returns whether name is a basic type, like int32, string, any, timestamp, etc.
func IsBasicType(name string) bool {
switch name {
case "any", "bool", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "int64s", "uint64s", "float32", "float64", "string", "timestamp":
return true
}
return false
}
type genError struct{ error }
func parseError(path string, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
err := fmt.Errorf("invalid sherpadoc at %s: %s", path, msg)
panic(genError{err})
}
func makePath(path string, field string, index int, name string) string {
return fmt.Sprintf("%s.%s[%d (%q)]", path, field, index, name)
}
// NOTE: sherpaweb/ts/parse.ts and sherpadoc/check.go contain the same checking.
// The code is very similar. Best keep it in sync and modify the implementations in tandem.
type checker struct {
types map[string]struct{}
functions map[string]struct{}
}
func (c checker) markIdent(path, ident string) {
if _, ok := c.types[ident]; ok {
parseError(path, "duplicate type %q", ident)
}
c.types[ident] = struct{}{}
}
func (c checker) walkTypeNames(path string, sec *Section) {
for i, t := range sec.Structs {
c.markIdent(makePath(path, "Structs", i, t.Name), t.Name)
}
for i, t := range sec.Ints {
npath := makePath(path, "Ints", i, t.Name)
c.markIdent(npath, t.Name)
for j, v := range t.Values {
c.markIdent(makePath(npath, "Values", j, v.Name), v.Name)
}
}
for i, t := range sec.Strings {
npath := makePath(path, "Strings", i, t.Name)
c.markIdent(npath, t.Name)
for j, v := range t.Values {
c.markIdent(makePath(npath, "Values", j, v.Name), v.Name)
}
}
for i, subsec := range sec.Sections {
c.walkTypeNames(makePath(path, "Sections", i, subsec.Name), subsec)
}
}
func (c checker) walkFunctionNames(path string, sec *Section) {
for i, fn := range sec.Functions {
npath := makePath(path, "Functions", i, fn.Name)
if _, ok := c.functions[fn.Name]; ok {
parseError(npath, "duplicate function %q", fn.Name)
}
c.functions[fn.Name] = struct{}{}
paramNames := map[string]struct{}{}
for i, arg := range fn.Params {
if _, ok := paramNames[arg.Name]; ok {
parseError(makePath(npath, "Params", i, arg.Name), "duplicate parameter name")
}
paramNames[arg.Name] = struct{}{}
}
returnNames := map[string]struct{}{}
for i, arg := range fn.Returns {
if _, ok := returnNames[arg.Name]; ok {
parseError(makePath(npath, "Returns", i, arg.Name), "duplicate return name")
}
returnNames[arg.Name] = struct{}{}
}
}
for i, subsec := range sec.Sections {
c.walkFunctionNames(makePath(path, "Sections", i, subsec.Name), subsec)
}
}
func (c checker) checkTypewords(path string, tokens []string, okNullable bool) {
if len(tokens) == 0 {
parseError(path, "unexpected end of typewords")
}
t := tokens[0]
tokens = tokens[1:]
if IsBasicType(t) {
if len(tokens) != 0 {
parseError(path, "leftover typewords %v", tokens)
}
return
}
switch t {
case "nullable":
if !okNullable {
parseError(path, "repeated nullable in typewords")
}
if len(tokens) == 0 {
parseError(path, "missing typeword after %#v", t)
}
c.checkTypewords(path, tokens, false)
case "[]", "{}":
if len(tokens) == 0 {
parseError(path, "missing typeword after %#v", t)
}
c.checkTypewords(path, tokens, true)
default:
_, ok := c.types[t]
if !ok {
parseError(path, "referenced type %q does not exist", t)
}
if len(tokens) != 0 {
parseError(path, "leftover typewords %v", tokens)
}
}
}
func (c checker) walkTypewords(path string, sec *Section) {
for i, t := range sec.Structs {
npath := makePath(path, "Structs", i, t.Name)
for j, f := range t.Fields {
c.checkTypewords(makePath(npath, "Fields", j, f.Name), f.Typewords, true)
}
}
for i, fn := range sec.Functions {
npath := makePath(path, "Functions", i, fn.Name)
for j, arg := range fn.Params {
c.checkTypewords(makePath(npath, "Params", j, arg.Name), arg.Typewords, true)
}
for j, arg := range fn.Returns {
c.checkTypewords(makePath(npath, "Returns", j, arg.Name), arg.Typewords, true)
}
}
for i, subsec := range sec.Sections {
c.walkTypewords(makePath(path, "Sections", i, subsec.Name), subsec)
}
}
// Check walks the sherpa section and checks it for correctness. It checks for:
//
// - Duplicate type names.
// - Duplicate parameter or return names.
// - References to types that are not defined.
// - Validity of typewords.
func Check(doc *Section) (retErr error) {
defer func() {
e := recover()
if e != nil {
g, ok := e.(genError)
if !ok {
panic(e)
}
retErr = error(g)
}
}()
c := checker{map[string]struct{}{}, map[string]struct{}{}}
c.walkTypeNames("", doc)
c.walkFunctionNames("", doc)
c.walkTypewords("", doc)
return nil
}