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 }