A codeless .bnp repo has no go.mod by design — mirror the ninja CLI classifier (core bnp.IsCodelessRepo) and skip both the standalone-plugin go.mod checks and the backend replace-directive parse for such roots. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
186 lines
4.8 KiB
Go
186 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
)
|
|
|
|
type pluginGoModViolation struct {
|
|
file string
|
|
line int
|
|
rule string
|
|
detail string
|
|
}
|
|
|
|
func currentCMSCoreSDKVersion() (string, error) {
|
|
return readRequiredModuleVersion(filepath.Join(blockNinjaRepoRoot(), "backend", "go.mod"), blockCoreImportPrefix)
|
|
}
|
|
|
|
func parseGoModForSafety(goModPath string) (*modfile.File, []pluginGoModViolation) {
|
|
if !fileExists(goModPath) {
|
|
return nil, []pluginGoModViolation{{
|
|
file: filepath.Base(goModPath),
|
|
rule: "missing-go-mod",
|
|
detail: fmt.Sprintf("missing %s", filepath.Base(goModPath)),
|
|
}}
|
|
}
|
|
|
|
data, err := os.ReadFile(goModPath)
|
|
if err != nil {
|
|
return nil, []pluginGoModViolation{{
|
|
file: filepath.Base(goModPath),
|
|
rule: "read-go-mod",
|
|
detail: fmt.Sprintf("failed to read go.mod: %v", err),
|
|
}}
|
|
}
|
|
|
|
parsed, err := modfile.Parse(goModPath, data, nil)
|
|
if err != nil {
|
|
return nil, []pluginGoModViolation{{
|
|
file: filepath.Base(goModPath),
|
|
rule: "parse-go-mod",
|
|
detail: fmt.Sprintf("failed to parse go.mod: %v", err),
|
|
}}
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
func newGoModReplaceViolation(parsed *modfile.File, replace *modfile.Replace, rule string) pluginGoModViolation {
|
|
detail := fmt.Sprintf("replace directive present for %s", replace.Old.Path)
|
|
if replace.New.Path != "" {
|
|
detail += fmt.Sprintf(" -> %s", replace.New.Path)
|
|
}
|
|
if replace.New.Version != "" {
|
|
detail += fmt.Sprintf(" %s", replace.New.Version)
|
|
}
|
|
|
|
line := 0
|
|
if replace.Syntax != nil {
|
|
line = replace.Syntax.Start.Line
|
|
}
|
|
|
|
return pluginGoModViolation{
|
|
file: filepath.Base(parsed.Syntax.Name),
|
|
line: line,
|
|
rule: rule,
|
|
detail: detail,
|
|
}
|
|
}
|
|
|
|
func checkGoModForAnyReplaceDirectives(goModPath string) []pluginGoModViolation {
|
|
parsed, violations := parseGoModForSafety(goModPath)
|
|
if parsed == nil {
|
|
return violations
|
|
}
|
|
|
|
for _, replace := range parsed.Replace {
|
|
violations = append(violations, newGoModReplaceViolation(parsed, replace, "no-replace-directives"))
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
func checkGoModForModuleReplaceDirective(goModPath, modulePath string) []pluginGoModViolation {
|
|
parsed, violations := parseGoModForSafety(goModPath)
|
|
if parsed == nil {
|
|
return violations
|
|
}
|
|
|
|
for _, replace := range parsed.Replace {
|
|
if replace.Old.Path != modulePath {
|
|
continue
|
|
}
|
|
violations = append(violations, newGoModReplaceViolation(parsed, replace, "no-local-block-core-replace"))
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
func readRequiredModuleVersion(goModPath, modulePath string) (string, error) {
|
|
data, err := os.ReadFile(goModPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parsed, err := modfile.Parse(goModPath, data, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, req := range parsed.Require {
|
|
if req.Mod.Path == modulePath {
|
|
return req.Mod.Version, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s does not require %s", goModPath, modulePath)
|
|
}
|
|
|
|
func checkCMSCoreSDKGoMod(root string) []pluginGoModViolation {
|
|
return checkGoModForModuleReplaceDirective(filepath.Join(root, "go.mod"), blockCoreImportPrefix)
|
|
}
|
|
|
|
// isCodelessPluginRepo mirrors the ninja CLI classifier (core
|
|
// cmd/ninja/internal/bnp.IsCodelessRepo): a plugin.mod with no Go source at
|
|
// the repo root is a codeless plugin (WO-WZ-020) — declarative-only, no
|
|
// go.mod and no SDK version to pin.
|
|
func isCodelessPluginRepo(root string) bool {
|
|
if !fileExists(filepath.Join(root, "plugin.mod")) {
|
|
return false
|
|
}
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".go") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkStandalonePluginGoMod(root, requiredSDKVersion string) []pluginGoModViolation {
|
|
if isCodelessPluginRepo(root) {
|
|
return nil
|
|
}
|
|
goModPath := filepath.Join(root, "go.mod")
|
|
parsed, violations := parseGoModForSafety(goModPath)
|
|
if parsed == nil {
|
|
if len(violations) == 1 && violations[0].rule == "missing-go-mod" {
|
|
violations[0].detail = "standalone plugins must be standalone Go modules with a repo-root go.mod"
|
|
}
|
|
return violations
|
|
}
|
|
|
|
violations = append(violations, checkGoModForAnyReplaceDirectives(goModPath)...)
|
|
|
|
currentSDKVersion := ""
|
|
for _, req := range parsed.Require {
|
|
if req.Mod.Path == blockCoreImportPrefix {
|
|
currentSDKVersion = req.Mod.Version
|
|
break
|
|
}
|
|
}
|
|
if currentSDKVersion == "" {
|
|
violations = append(violations, pluginGoModViolation{
|
|
file: "go.mod",
|
|
rule: "missing-block-core-require",
|
|
detail: fmt.Sprintf("missing required module %s", blockCoreImportPrefix),
|
|
})
|
|
return violations
|
|
}
|
|
|
|
if requiredSDKVersion != "" && currentSDKVersion != requiredSDKVersion {
|
|
violations = append(violations, pluginGoModViolation{
|
|
file: "go.mod",
|
|
rule: "block-core-version-mismatch",
|
|
detail: fmt.Sprintf("requires %s, want %s", currentSDKVersion, requiredSDKVersion),
|
|
})
|
|
}
|
|
|
|
return violations
|
|
}
|