Files
mcp/internal/rules/env_rules.go
T

83 lines
2.5 KiB
Go
Raw Normal View History

package rules
import (
"go/ast"
"reflect"
"strings"
)
// knownEnvVars is the set of every real framework env-var name, injected once at
// server startup from the index (see rules.SetKnownEnvVars). When empty — e.g.
// in unit tests that exercise rules without an index — config.unknown-env-var is
// a no-op, so it never fires on incomplete knowledge.
var knownEnvVars map[string]struct{}
// SetKnownEnvVars installs the authoritative set of framework env-var names that
// backs config.unknown-env-var. Call once before serving; the server derives the
// set from the index so the rule can never drift from the real struct tags.
func SetKnownEnvVars(names map[string]struct{}) {
knownEnvVars = names
}
func init() {
registered = append(registered,
Rule{
ID: "config.unknown-env-var",
Severity: SeverityError,
Module: "wire",
Check: checkUnknownEnvVar,
},
)
}
// checkUnknownEnvVar flags any struct field tagged with an EINHERJAR_* env var
// that the framework does not actually declare — the exact class of drift that
// shipped EINHERJAR_PG_DATABASE (real name: _PG_NAME) and EINHERJAR_SERVER_ADDR
// (real: _SERVER_HOST/_PORT). App-owned prefixes (APP_*) are never flagged.
func checkUnknownEnvVar(c *Context) []Finding {
if len(knownEnvVars) == 0 {
return nil
}
var hits []Finding
ast.Inspect(c.File, func(n ast.Node) bool {
st, ok := n.(*ast.StructType)
if !ok || st.Fields == nil {
return true
}
for _, field := range st.Fields.List {
if field.Tag == nil {
continue
}
name, ok := envName(strings.Trim(field.Tag.Value, "`"))
if !ok || !strings.HasPrefix(name, "EINHERJAR_") {
continue
}
if _, real := knownEnvVars[name]; real {
continue
}
hits = append(hits, Finding{
Message: name + " is not a real Einherjar env var (invented or misspelled)",
Hint: "Verify the exact tag with get_config_env. The framework never invents EINHERJAR_* names; compose the component's real Config or fix the tag.",
Line: c.Fset.Position(field.Tag.Pos()).Line,
})
}
return true
})
return hits
}
// envName extracts the env-var name from a raw (backtick-stripped) struct tag,
// dropping the ,required / ,unset options. ok is false when there is no env key
// or it is "-".
func envName(tag string) (string, bool) {
raw, present := reflect.StructTag(tag).Lookup("env")
if !present {
return "", false
}
name := strings.TrimSpace(strings.Split(raw, ",")[0])
if name == "" || name == "-" {
return "", false
}
return name, true
}