gps/constraint.go

Summary

Maintainability
A
0 mins
Test Coverage
// 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"
    "github.com/golang/dep/gps/internal/pb"
)

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

    // copyTo copies fields into a serializable representation which can be
    // converted back into an identical Constraint with constraintFromCache.
    copyTo(*pb.Constraint)

    // 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
}

// constraintFromCache returns a Constraint identical to the one which produced m.
func constraintFromCache(m *pb.Constraint) (Constraint, error) {
    switch m.Type {
    case pb.Constraint_Revision:
        return Revision(m.Value), nil
    case pb.Constraint_Branch:
        return NewBranch(m.Value), nil
    case pb.Constraint_DefaultBranch:
        return newDefaultBranch(m.Value), nil
    case pb.Constraint_Version:
        return plainVersion(m.Value), nil
    case pb.Constraint_Semver:
        return NewSemverConstraint(m.Value)

    default:
        return nil, fmt.Errorf("unrecognized Constraint type: %#v", m)
    }
}

// unpairedVersionFromCache returns an UnpairedVersion identical to the one which produced m.
func unpairedVersionFromCache(m *pb.Constraint) (UnpairedVersion, error) {
    switch m.Type {
    case pb.Constraint_Branch:
        return NewBranch(m.Value), nil
    case pb.Constraint_DefaultBranch:
        return newDefaultBranch(m.Value), nil
    case pb.Constraint_Version:
        return plainVersion(m.Value), nil
    case pb.Constraint_Semver:
        sv, err := semver.NewVersion(m.Value)
        if err != nil {
            return nil, err
        }
        return semVersion{sv: sv}, nil

    default:
        return nil, fmt.Errorf("unrecognized UnpairedVersion type: %#v", m)
    }
}

// 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 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 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()
}

func (c semverConstraint) copyTo(msg *pb.Constraint) {
    msg.Type = pb.Constraint_Semver
    msg.Value = 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)
}

func (anyConstraint) copyTo(*pb.Constraint) {
    panic("anyConstraint should never be serialized; it is solver internal-only")
}

// 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
}

func (noneConstraint) copyTo(*pb.Constraint) {
    panic("noneConstraint should never be serialized; it is solver internal-only")
}

// 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
}

// 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
}