dep/internal/gps/constraints.go

396 строки
10 KiB
Go

// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gps
import (
"fmt"
"sort"
"github.com/Masterminds/semver"
)
var (
none = noneConstraint{}
any = anyConstraint{}
)
// A Constraint provides structured limitations on the versions that are
// admissible for a given project.
//
// As with Version, it has a private method because the gps's internal
// implementation of the problem is complete, and the system relies on type
// magic to operate.
type Constraint interface {
fmt.Stringer
// ImpliedCaretString converts the Constraint to a string in the same manner
// as String(), but treats the empty operator as equivalent to ^, rather
// than =.
//
// In the same way that String() is the inverse of NewConstraint(), this
// method is the inverse of NewSemverConstraintIC().
ImpliedCaretString() string
// Matches indicates if the provided Version is allowed by the Constraint.
Matches(Version) bool
// MatchesAny indicates if the intersection of the Constraint with the
// provided Constraint would yield a Constraint that could allow *any*
// Version.
MatchesAny(Constraint) bool
// Intersect computes the intersection of the Constraint with the provided
// Constraint.
Intersect(Constraint) Constraint
// typedString emits the normal stringified representation of the provided
// constraint, prefixed with a string that uniquely identifies the type of
// the constraint.
//
// It also forces Constraint to be a private/sealed interface, which is a
// design goal of the system.
typedString() string
// identical returns true if the constraints are identical.
//
// Identical Constraints behave identically for all methods defined by the
// interface. A Constraint is always identical to itself.
//
// Constraints serialized for caching are de-serialized into identical instances.
identical(Constraint) bool
}
// NewSemverConstraint attempts to construct a semver Constraint object from the
// input string.
//
// If the input string cannot be made into a valid semver Constraint, an error
// is returned.
func NewSemverConstraint(body string) (Constraint, error) {
c, err := semver.NewConstraint(body)
if err != nil {
return nil, err
}
// If we got a simple semver.Version, simplify by returning our
// corresponding type
if sv, ok := c.(semver.Version); ok {
return semVersion{sv: sv}, nil
}
return semverConstraint{c: c}, nil
}
// NewSemverConstraintIC attempts to construct a semver Constraint object from the
// input string, defaulting to a caret, ^, when no operator is specified. Put
// differently, ^ is the default operator for NewSemverConstraintIC, while =
// is the default operator for NewSemverConstraint.
//
// If the input string cannot be made into a valid semver Constraint, an error
// is returned.
func NewSemverConstraintIC(body string) (Constraint, error) {
c, err := semver.NewConstraintIC(body)
if err != nil {
return nil, err
}
// If we got a simple semver.Version, simplify by returning our
// corresponding type
if sv, ok := c.(semver.Version); ok {
return semVersion{sv: sv}, nil
}
return semverConstraint{c: c}, nil
}
type semverConstraint struct {
c semver.Constraint
}
func (c semverConstraint) String() string {
return c.c.String()
}
// ImpliedCaretString converts the Constraint to a string in the same manner
// as String(), but treats the empty operator as equivalent to ^, rather
// than =.
//
// In the same way that String() is the inverse of NewConstraint(), this
// method is the inverse of NewSemverConstraintIC().
func (c semverConstraint) ImpliedCaretString() string {
return c.c.ImpliedCaretString()
}
func (c semverConstraint) typedString() string {
return fmt.Sprintf("svc-%s", c.c.String())
}
func (c semverConstraint) Matches(v Version) bool {
switch tv := v.(type) {
case versionTypeUnion:
for _, elem := range tv {
if c.Matches(elem) {
return true
}
}
case semVersion:
return c.c.Matches(tv.sv) == nil
case versionPair:
if tv2, ok := tv.v.(semVersion); ok {
return c.c.Matches(tv2.sv) == nil
}
}
return false
}
func (c semverConstraint) MatchesAny(c2 Constraint) bool {
return c.Intersect(c2) != none
}
func (c semverConstraint) Intersect(c2 Constraint) Constraint {
switch tc := c2.(type) {
case anyConstraint:
return c
case versionTypeUnion:
for _, elem := range tc {
if rc := c.Intersect(elem); rc != none {
return rc
}
}
case semverConstraint:
rc := c.c.Intersect(tc.c)
if !semver.IsNone(rc) {
return semverConstraint{c: rc}
}
case semVersion:
rc := c.c.Intersect(tc.sv)
if !semver.IsNone(rc) {
// If single version intersected with constraint, we know the result
// must be the single version, so just return it back out
return c2
}
case versionPair:
if tc2, ok := tc.v.(semVersion); ok {
rc := c.c.Intersect(tc2.sv)
if !semver.IsNone(rc) {
// same reasoning as previous case
return c2
}
}
}
return none
}
func (c semverConstraint) identical(c2 Constraint) bool {
sc2, ok := c2.(semverConstraint)
if !ok {
return false
}
return c.c.String() == sc2.c.String()
}
// IsAny indicates if the provided constraint is the wildcard "Any" constraint.
func IsAny(c Constraint) bool {
_, ok := c.(anyConstraint)
return ok
}
// Any returns a constraint that will match anything.
func Any() Constraint {
return anyConstraint{}
}
// anyConstraint is an unbounded constraint - it matches all other types of
// constraints. It mirrors the behavior of the semver package's any type.
type anyConstraint struct{}
func (anyConstraint) String() string {
return "*"
}
func (anyConstraint) ImpliedCaretString() string {
return "*"
}
func (anyConstraint) typedString() string {
return "any-*"
}
func (anyConstraint) Matches(Version) bool {
return true
}
func (anyConstraint) MatchesAny(Constraint) bool {
return true
}
func (anyConstraint) Intersect(c Constraint) Constraint {
return c
}
func (anyConstraint) identical(c Constraint) bool {
return IsAny(c)
}
// noneConstraint is the empty set - it matches no versions. It mirrors the
// behavior of the semver package's none type.
type noneConstraint struct{}
func (noneConstraint) String() string {
return ""
}
func (noneConstraint) ImpliedCaretString() string {
return ""
}
func (noneConstraint) typedString() string {
return "none-"
}
func (noneConstraint) Matches(Version) bool {
return false
}
func (noneConstraint) MatchesAny(Constraint) bool {
return false
}
func (noneConstraint) Intersect(Constraint) Constraint {
return none
}
func (noneConstraint) identical(c Constraint) bool {
_, ok := c.(noneConstraint)
return ok
}
// A ProjectConstraint combines a ProjectIdentifier with a Constraint. It
// indicates that, if packages contained in the ProjectIdentifier enter the
// depgraph, they must do so at a version that is allowed by the Constraint.
type ProjectConstraint struct {
Ident ProjectIdentifier
Constraint Constraint
}
// ProjectConstraints is a map of projects, as identified by their import path
// roots (ProjectRoots) to the corresponding ProjectProperties.
//
// They are the standard form in which Manifests declare their required
// dependency properties - constraints and network locations - as well as the
// form in which RootManifests declare their overrides.
type ProjectConstraints map[ProjectRoot]ProjectProperties
type workingConstraint struct {
Ident ProjectIdentifier
Constraint Constraint
overrNet, overrConstraint bool
}
func pcSliceToMap(l []ProjectConstraint, r ...[]ProjectConstraint) ProjectConstraints {
final := make(ProjectConstraints)
for _, pc := range l {
final[pc.Ident.ProjectRoot] = ProjectProperties{
Source: pc.Ident.Source,
Constraint: pc.Constraint,
}
}
for _, pcs := range r {
for _, pc := range pcs {
if pp, exists := final[pc.Ident.ProjectRoot]; exists {
// Technically this should be done through a bridge for
// cross-version-type matching...but this is a one off for root and
// that's just ridiculous for this.
pp.Constraint = pp.Constraint.Intersect(pc.Constraint)
final[pc.Ident.ProjectRoot] = pp
} else {
final[pc.Ident.ProjectRoot] = ProjectProperties{
Source: pc.Ident.Source,
Constraint: pc.Constraint,
}
}
}
}
return final
}
func (m ProjectConstraints) asSortedSlice() []ProjectConstraint {
pcs := make([]ProjectConstraint, len(m))
k := 0
for pr, pp := range m {
pcs[k] = ProjectConstraint{
Ident: ProjectIdentifier{
ProjectRoot: pr,
Source: pp.Source,
},
Constraint: pp.Constraint,
}
k++
}
sort.SliceStable(pcs, func(i, j int) bool {
return pcs[i].Ident.Less(pcs[j].Ident)
})
return pcs
}
// overrideAll treats the receiver ProjectConstraints map as a set of override
// instructions, and applies overridden values to the ProjectConstraints.
//
// A slice of workingConstraint is returned, allowing differentiation between
// values that were or were not overridden.
func (m ProjectConstraints) overrideAll(pcm ProjectConstraints) (out []workingConstraint) {
out = make([]workingConstraint, len(pcm))
k := 0
for pr, pp := range pcm {
out[k] = m.override(pr, pp)
k++
}
sort.SliceStable(out, func(i, j int) bool {
return out[i].Ident.Less(out[j].Ident)
})
return
}
// override replaces a single ProjectConstraint with a workingConstraint,
// overriding its values if a corresponding entry exists in the
// ProjectConstraints map.
func (m ProjectConstraints) override(pr ProjectRoot, pp ProjectProperties) workingConstraint {
wc := workingConstraint{
Ident: ProjectIdentifier{
ProjectRoot: pr,
Source: pp.Source,
},
Constraint: pp.Constraint,
}
if opp, has := m[pr]; has {
// The rule for overrides is that *any* non-zero value for the prop
// should be considered an override, even if it's equal to what's
// already there.
if opp.Constraint != nil {
wc.Constraint = opp.Constraint
wc.overrConstraint = true
}
// This may appear incorrect, because the solver encodes meaning into
// the empty string for NetworkName (it means that it would use the
// import path by default, but could be coerced into using an alternate
// URL). However, that 'coercion' can only happen if there's a
// disagreement between projects on where a dependency should be sourced
// from. Such disagreement is exactly what overrides preclude, so
// there's no need to preserve the meaning of "" here - thus, we can
// treat it as a zero value and ignore it, rather than applying it.
if opp.Source != "" {
wc.Ident.Source = opp.Source
wc.overrNet = true
}
}
return wc
}