feat: expose selected version to formulas - #178
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Review: target.version auto-property
The change is well-structured and well-tested. The matrixTarget → formulaTarget rename is clean (no stale references remain), the new Version() accessor is documented, Target() keeps its defensive copy, and both cache paths in at() inject identically. The new TestFormulaModule_AtInjectsTargetVersion gives solid end-to-end coverage of target.version flowing into filter, onRequire, and onBuild. Build and package tests pass.
No blocking issues. The findings below are minor/optional cleanups.
A note on the hot path (pre-existing, not introduced here): at() re-walks and re-parses every formula file via findMaxFromVer on each lookup, and formula.Clone re-runs the interpreted Main() under the module mutex. This PR only makes that path slightly hotter by adding one reflection write. Worth keeping in mind if formula resolution ever becomes a bottleneck, but out of scope for this change.
Additional findings
internal/modules/target.go:46: [P2] Duplicate setValue helper risks silent divergence: ThissetValueis byte-for-byte identical to the one ininternal/formula/utils.go. These are the trickiest helpers in the codebase (unsafe unexported-field access, nil→zero semantics), and two copies can silently drift — if one later changes how nil or the unexported field is handled,injectTargetwould no longer match the loader's semantics. Consider exportingSetValue/ValueOffrominternal/formula(or a shared internal package) and reusing it here.internal/modules/target.go:28: [P3] Options seeded from DefaultOptions is subtle; add a comment:effective.Optionsis initialized frommaps.Clone(target.DefaultOptions), discarding whatevertarget.Optionsalready held, thentarget.DefaultOptionsis cloned again on the next line. TodayDefaults()is the onlyOptionsmutator and keeps both fields in lockstep, so the behavior is correct — but the intent is non-obvious (target.Optionsis what the DSL reads back,DefaultOptionsis a separate axis), and the doublemaps.CloneofDefaultOptionsis redundant when it is non-empty. A one-line comment ("seed active options from defaults, then overlay the build matrix") plus collapsing the double clone would help. If a future DSL setter ever writesOptionsindependently, this line would silently drop it.
| func (p *ModuleF) Target() formulaTarget { | ||
| return formulaTarget{ | ||
| version: p.targetVersion, | ||
| m: Matrix{ | ||
| Require: maps.Clone(p.target.Require), | ||
| Options: maps.Clone(p.target.Options), | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
[P3] Target() lacks a doc comment on its defensive-copy contract
Unlike Version()/Require()/Options(), Target() has no doc comment. It returns a defensive copy (map-level maps.Clone of Require/Options, dropping DefaultOptions), which TestModuleF_TargetReturnsMatrixCopy relies on. Note the clone is shallow: the inner []string slices are shared, so in-place element mutation (Target().Options()["k"][0] = ...) would still reach internal state — the test only covers whole-slice reassignment and new keys. A short doc note clarifying the copy contract (and its shallowness) would prevent misreads.
| for key, values := range matrix.Options { | ||
| effective.Options[key] = slices.Clone(values) | ||
| } | ||
| setValue(structElem, "targetVersion", version) |
There was a problem hiding this comment.
[P3] target.version re-exposes an untrusted upstream string to the DSL
version is written into the formula struct with no validation. It crosses a trust boundary: it can originate from git tags/refs discovered on a remote repository (attacker-controlled) or MVS-resolved dependency versions. Once exposed via target.version, formula authors commonly forward it into sinks such as deps.require "owner/mod", target.version and ctx.setMetadata target.version, which reach VCS refs and filesystem paths (the MkdirTemp path already escapes /). The module path is validated elsewhere, but the version is not at this injection point. This is defense-in-depth — the value already flowed through the same channels before this PR — but it is worth documenting that target.version is not trusted input so downstream sinks validate/escape it.
Summary
target.versionTesting
go test -ldflags="-checklinkname=0" ./formula -count=1go test -ldflags="-checklinkname=0" ./internal/ixgo -count=1go test -ldflags="-checklinkname=0" ./internal/formula -count=1go test -ldflags="-checklinkname=0" ./internal/modules -count=1