241 lines
8.2 KiB
Go
241 lines
8.2 KiB
Go
package apigen
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/format"
|
|
goparser "go/parser"
|
|
"go/scanner"
|
|
gotoken "go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type logicMethod struct {
|
|
path, signature string
|
|
line int
|
|
known bool
|
|
}
|
|
|
|
// checkLogic inspects source only: no project code, init hooks, Go commands or
|
|
// dependency downloads are executed. Seeds participate only when files do not
|
|
// already exist, matching apply's ownership rules.
|
|
func checkLogic(root, module string, contract *contractSnapshot, seeds map[string][]byte) ([]Diagnostic, map[string]logicMethod) {
|
|
groups := map[string][]routeContract{}
|
|
knownTypes := map[string]bool{}
|
|
for _, r := range contract.Routes {
|
|
if !r.Manual {
|
|
groups[r.Group] = append(groups[r.Group], r)
|
|
}
|
|
for name := range r.Types {
|
|
knownTypes[name] = true
|
|
}
|
|
}
|
|
// Project-owned aliases may make differently named DTOs identical. Let
|
|
// the compiler handle such cases rather than rejecting valid Go aliases.
|
|
typeExtensions := false
|
|
if entries, err := os.ReadDir(filepath.Join(root, "internal/types")); err == nil {
|
|
for _, e := range entries {
|
|
if sourceFile(e.Name()) && !ownedPath("internal/types/"+e.Name()) {
|
|
typeExtensions = true
|
|
}
|
|
}
|
|
}
|
|
var diagnostics []Diagnostic
|
|
locations := map[string]logicMethod{}
|
|
for group, routes := range groups {
|
|
directory := "internal/logic/" + group
|
|
files := map[string][]byte{}
|
|
uncertain := false
|
|
entries, err := os.ReadDir(filepath.Join(root, filepath.FromSlash(directory)))
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
diagnostics = append(diagnostics, Diagnostic{Severity: "error", Code: "logicUnreadable", Path: directory, Message: err.Error()})
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
if e.IsDir() || !sourceFile(e.Name()) {
|
|
continue
|
|
}
|
|
name := directory + "/" + e.Name()
|
|
if err := safePath(root, name); err != nil {
|
|
diagnostics = append(diagnostics, Diagnostic{Severity: "error", Code: "logicUnreadable", Path: name, Message: err.Error()})
|
|
uncertain = true
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(name)))
|
|
if err != nil {
|
|
diagnostics = append(diagnostics, Diagnostic{Severity: "error", Code: "logicUnreadable", Path: name, Message: err.Error()})
|
|
uncertain = true
|
|
continue
|
|
}
|
|
files[name] = data
|
|
}
|
|
for name, data := range seeds {
|
|
if filepath.ToSlash(filepath.Dir(name)) == directory && sourceFile(filepath.Base(name)) {
|
|
if _, exists := files[name]; !exists {
|
|
files[name] = data
|
|
}
|
|
}
|
|
}
|
|
methods := map[string][]logicMethod{}
|
|
var names []string
|
|
for name := range files {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
data := files[name]
|
|
// Do not guess the user's GOFLAGS/build-tag combination. Flag the
|
|
// group for compiler verification if conditional files are present.
|
|
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(name))); err == nil {
|
|
match, err := build.Default.MatchFile(filepath.Join(root, filepath.FromSlash(directory)), filepath.Base(name))
|
|
if err != nil || !match {
|
|
uncertain = true
|
|
continue
|
|
}
|
|
}
|
|
positions := gotoken.NewFileSet()
|
|
file, err := goparser.ParseFile(positions, name, data, goparser.SkipObjectResolution|goparser.ParseComments)
|
|
if err != nil {
|
|
d := Diagnostic{Severity: "error", Code: "logicSyntax", Path: name, Message: "cannot parse Logic source: " + err.Error()}
|
|
if list, ok := err.(scanner.ErrorList); ok && len(list) > 0 {
|
|
d.Line = list[0].Pos.Line
|
|
}
|
|
diagnostics = append(diagnostics, d)
|
|
uncertain = true
|
|
continue
|
|
}
|
|
conditional := false
|
|
for _, group := range file.Comments {
|
|
for _, comment := range group.List {
|
|
if comment.Pos() < file.Package && (strings.HasPrefix(comment.Text, "//go:build ") || strings.HasPrefix(comment.Text, "// +build ")) {
|
|
conditional = true
|
|
}
|
|
}
|
|
}
|
|
if conditional {
|
|
uncertain = true
|
|
continue
|
|
}
|
|
imports := map[string]string{}
|
|
for _, imp := range file.Imports {
|
|
path, err := strconv.Unquote(imp.Path.Value)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
alias := filepath.Base(path)
|
|
if imp.Name != nil {
|
|
alias = imp.Name.Name
|
|
}
|
|
imports[alias] = path
|
|
}
|
|
for _, decl := range file.Decls {
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fn.Recv == nil || len(fn.Recv.List) != 1 {
|
|
continue
|
|
}
|
|
receiver := fn.Recv.List[0].Type
|
|
if pointer, ok := receiver.(*ast.StarExpr); ok {
|
|
receiver = pointer.X
|
|
}
|
|
id, ok := receiver.(*ast.Ident)
|
|
if !ok || id.Name != "Logic" {
|
|
continue
|
|
}
|
|
params, a := canonicalFields(fn.Type.Params, imports, module, knownTypes, typeExtensions)
|
|
results, b := canonicalFields(fn.Type.Results, imports, module, knownTypes, typeExtensions)
|
|
methods[fn.Name.Name] = append(methods[fn.Name.Name], logicMethod{name, displaySignature(fn.Name.Name, params, results), positions.Position(fn.Pos()).Line, a && b})
|
|
}
|
|
}
|
|
for _, r := range routes {
|
|
found := methods[upper(r.Handler)]
|
|
if len(found) == 1 && !uncertain {
|
|
locations[r.file()] = found[0]
|
|
}
|
|
d := Diagnostic{Path: r.file(), Operation: r.operation(), After: r.Signature}
|
|
switch {
|
|
case uncertain:
|
|
d.Severity, d.Code, d.Message = "review", "logicUnchecked", "conditional or unreadable source prevents a complete signature check; run go test ./... with your build settings"
|
|
case len(found) == 0:
|
|
d.Severity, d.Code, d.Message = "review", "logicNotFound", "no direct Logic method found; check embedded/aliased implementations with go test ./..."
|
|
case len(found) > 1:
|
|
d.Severity, d.Code, d.Message = "error", "logicDuplicate", "multiple Logic methods have this name"
|
|
for _, m := range found {
|
|
d.Details = append(d.Details, fmt.Sprintf("%s:%d", m.path, m.line))
|
|
}
|
|
case !found[0].known:
|
|
d.Severity, d.Code, d.Message = "review", "logicUnchecked", "signature uses project-specific type names; verify aliases with go test ./..."
|
|
d.Path, d.Line, d.Before = found[0].path, found[0].line, found[0].signature
|
|
case found[0].signature != r.Signature:
|
|
d.Severity, d.Code, d.Message = "error", "logicSignature", "preserved Logic signature differs from the generated call; update this method"
|
|
d.Path, d.Line, d.Before = found[0].path, found[0].line, found[0].signature
|
|
default:
|
|
continue
|
|
}
|
|
diagnostics = append(diagnostics, d)
|
|
}
|
|
}
|
|
return diagnostics, locations
|
|
}
|
|
|
|
func sourceFile(name string) bool {
|
|
return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") && !strings.HasPrefix(name, ".") && !strings.HasPrefix(name, "_")
|
|
}
|
|
|
|
func canonicalFields(fields *ast.FieldList, imports map[string]string, module string, knownTypes map[string]bool, extensions bool) ([]string, bool) {
|
|
var values []string
|
|
known := true
|
|
if fields != nil {
|
|
for _, field := range fields.List {
|
|
value, ok := canonicalType(field.Type, imports, module, knownTypes, extensions)
|
|
known = known && ok
|
|
count := len(field.Names)
|
|
if count == 0 {
|
|
count = 1
|
|
}
|
|
for i := 0; i < count; i++ {
|
|
values = append(values, value)
|
|
}
|
|
}
|
|
}
|
|
return values, known
|
|
}
|
|
|
|
func canonicalType(expr ast.Expr, imports map[string]string, module string, knownTypes map[string]bool, extensions bool) (string, bool) {
|
|
switch e := expr.(type) {
|
|
case *ast.ParenExpr:
|
|
return canonicalType(e.X, imports, module, knownTypes, extensions)
|
|
case *ast.StarExpr:
|
|
value, ok := canonicalType(e.X, imports, module, knownTypes, extensions)
|
|
return "*" + value, ok
|
|
case *ast.Ellipsis:
|
|
value, ok := canonicalType(e.Elt, imports, module, knownTypes, extensions)
|
|
return "..." + value, ok
|
|
case *ast.Ident:
|
|
switch e.Name {
|
|
case "error", "bool", "string", "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64", "complex64", "complex128", "any":
|
|
return e.Name, true
|
|
}
|
|
case *ast.SelectorExpr:
|
|
if pkg, ok := e.X.(*ast.Ident); ok {
|
|
switch imports[pkg.Name] {
|
|
case "context":
|
|
return "context." + e.Sel.Name, true
|
|
case frameworkModule + "/httpx":
|
|
return "httpx." + e.Sel.Name, true
|
|
case module + "/internal/types":
|
|
return "types." + e.Sel.Name, knownTypes[e.Sel.Name] || !extensions
|
|
}
|
|
}
|
|
}
|
|
var b bytes.Buffer
|
|
_ = format.Node(&b, gotoken.NewFileSet(), expr)
|
|
return b.String(), false
|
|
}
|