1package main23import (4 "go/ast"5 "go/token"6 "regexp"7 "strconv"8 "strings"910 "golang.org/x/tools/go/analysis"11 "golang.org/x/tools/go/analysis/passes/inspect"12 "golang.org/x/tools/go/ast/inspector"13)1415// semconvLiterals keeps the conventions registry the single place an `atlas.*`16// key is spelled. Producers are strict — an unregistered key fails the build —17// and that check only means something if every writer goes through the18// registry API (issue #5 §2, §9).19var semconvLiterals = &analysis.Analyzer{20 Name: "semconvlit",21 Doc: `forbid loose atlas.* convention keys outside format/semconv (issue #5 §9)2223Convention keys are written through the format/semconv registry, never as24string literals. The registry owns the stability tier, the value checker and25the prose twin; a literal elsewhere silently opts out of all three.`,26 Requires: []*analysis.Analyzer{inspect.Analyzer},27 Run: runSemconvLiterals,28}2930// semconvRegistry is the package that may spell keys, because spelling them is31// what it is for.32const semconvRegistry = "format/semconv"3334// atlasKey matches a dotted convention key: atlas.render.as, atlas.hydro.huc12.35// A bare "atlas" or a sentence containing one is not a key, and neither is the36// manifest filename "atlas.json" — every registered key has at least two37// segments after the prefix.38var atlasKey = regexp.MustCompile(`^atlas(\.[a-z0-9][a-z0-9_]*){2,}$`)3940func runSemconvLiterals(pass *analysis.Pass) (any, error) {41 if testBinary(pass) {42 return nil, nil43 }44 fromRel, ok := rel(packagePath(pass))45 if !ok {46 return nil, nil47 }48 from := laneOf(fromRel)49 switch {50 case from == LaneOutside, from == LaneTests:51 // The test tree reads bundles other code produced; data says keys52 // out loud, and a reader is lenient by convention.53 return nil, nil54 case under(fromRel, semconvRegistry):55 return nil, nil56 }5758 insp, _ := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)59 if insp == nil {60 return nil, nil61 }62 r := newReporter(pass)63 insp.Preorder([]ast.Node{(*ast.BasicLit)(nil)}, func(n ast.Node) {64 lit := n.(*ast.BasicLit)65 if lit.Kind != token.STRING {66 return67 }68 value, err := strconv.Unquote(lit.Value)69 if err != nil || !atlasKey.MatchString(strings.TrimSpace(value)) {70 return71 }72 r.reportf(lit.Pos(), contractf(73 "the convention key "+quote(value)+" is written as a string literal outside "+semconvRegistry,74 "9",75 "conventions are asymmetric — producers strict, readers lenient — and the strictness lives in the registry: a literal key skips the value checker, the stability tier and the prose twin that documents it",76 ))77 })78 return nil, nil79}