1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-07 23:35:16 +00:00

lock: accept name[extras]==version; error on dependency-bearing options (-e/-r/-c)

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-06-10 14:35:36 +00:00
parent 5830434434
commit 9a7dc02643
No known key found for this signature in database
2 changed files with 45 additions and 5 deletions

View file

@ -28,11 +28,17 @@ type Requirement struct {
}
var (
// pinRE matches "name==version" optionally followed by extras/markers we
// ignore for matching purposes.
pinRE = regexp.MustCompile(`^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*==\s*([^\s;]+)`)
// pinRE matches "name[extras]==version". Extras are accepted (and ignored
// for matching, since a resolved lock pins their dependencies separately);
// environment markers after ";" are also ignored.
pinRE = regexp.MustCompile(`^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*==\s*([^\s;]+)`)
hashRE = regexp.MustCompile(`--hash=sha256:([0-9a-fA-F]{64})`)
normRE = regexp.MustCompile(`[-_.]+`)
// dependencyOptions reference further requirements rather than configuring
// the resolver. Silently skipping them could omit dependencies, so we
// reject them: a lock fed to pymage must be fully inlined and pinned.
dependencyOptions = []string{"-e", "--editable", "-r", "--requirement", "-c", "--constraint"}
)
// NormalizeName implements PEP 503 name normalization.
@ -66,9 +72,11 @@ func Parse(r io.Reader) ([]Requirement, error) {
if strings.TrimSpace(stripped) == "" {
return nil
}
// Skip pip options like -r/-c/--index-url that aren't pins.
// Configuration options (e.g. --index-url) aren't pins and are skipped,
// but options that pull in other requirements must not be silently
// ignored.
if isOption(stripped) {
return nil
return optionError(stripped)
}
m := pinRE.FindStringSubmatch(stripped)
if m == nil {
@ -127,6 +135,18 @@ func isOption(s string) bool {
return strings.HasPrefix(t, "-")
}
// optionError returns an error for dependency-bearing options (which we cannot
// honor from a local wheelhouse), and nil for benign configuration options.
func optionError(s string) error {
t := strings.TrimSpace(s)
for _, opt := range dependencyOptions {
if t == opt || strings.HasPrefix(t, opt+" ") || strings.HasPrefix(t, opt+"=") {
return fmt.Errorf("lock: unsupported option %q; referenced requirements must be inlined and pinned", t)
}
}
return nil
}
// Validate ensures every requirement is pinned and (optionally) hashed.
func Validate(reqs []Requirement, requireHashes bool) error {
for _, r := range reqs {

View file

@ -65,6 +65,10 @@ func TestParseRejectsUnsupportedLines(t *testing.T) {
"requests>=2.0", // not pinned with ==
"mypkg @ https://example.com/x.whl", // direct URL reference
"justaname", // bare name
"-e ./local", // editable reference
"--editable=./local", // editable reference
"-r other.txt", // nested requirements
"-c constraints.txt", // nested constraints
} {
if _, err := Parse(strings.NewReader(in)); err == nil {
t.Errorf("expected error for unsupported line %q", in)
@ -72,6 +76,22 @@ func TestParseRejectsUnsupportedLines(t *testing.T) {
}
}
func TestParseAllowsExtrasAndBenignOptions(t *testing.T) {
in := `--index-url https://pypi.org/simple
requests[socks,security]==2.31.0 --hash=sha256:` + strings.Repeat("a", 64) + `
`
reqs, err := Parse(strings.NewReader(in))
if err != nil {
t.Fatal(err)
}
if len(reqs) != 1 || reqs[0].Name != "requests" || reqs[0].Version != "2.31.0" {
t.Fatalf("got %+v", reqs)
}
if len(reqs[0].Hashes) != 1 {
t.Errorf("expected the hash to be parsed, got %v", reqs[0].Hashes)
}
}
func TestNormalizeName(t *testing.T) {
for _, tc := range []struct{ in, want string }{
{"Flask", "flask"},