github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/suggest/maven.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package suggest
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"slices"
    22  	"strings"
    23  
    24  	"deps.dev/util/resolve"
    25  	"deps.dev/util/semver"
    26  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest"
    27  	mavenmanifest "github.com/google/osv-scalibr/guidedremediation/internal/manifest/maven"
    28  	"github.com/google/osv-scalibr/guidedremediation/options"
    29  	"github.com/google/osv-scalibr/guidedremediation/result"
    30  	"github.com/google/osv-scalibr/guidedremediation/upgrade"
    31  	"github.com/google/osv-scalibr/internal/mavenutil"
    32  	"github.com/google/osv-scalibr/log"
    33  )
    34  
    35  // MavenSuggester suggests update patch for Maven dependencies.
    36  type MavenSuggester struct{}
    37  
    38  // Suggest returns the Patch to update Maven dependencies to a newer
    39  // version based on the options.
    40  func (ms *MavenSuggester) Suggest(ctx context.Context, mf manifest.Manifest, opts options.UpdateOptions) (result.Patch, error) {
    41  	specific, ok := mf.EcosystemSpecific().(mavenmanifest.ManifestSpecific)
    42  	if !ok {
    43  		return result.Patch{}, errors.New("invalid Maven ManifestSpecific data")
    44  	}
    45  
    46  	var packageUpdates []result.PackageUpdate
    47  	updated := make(map[resolve.VersionKey]bool)
    48  	for _, req := range append(mf.Requirements(), specific.RequirementsForUpdates...) {
    49  		if opts.UpgradeConfig.Get(req.Name) == upgrade.None {
    50  			continue
    51  		}
    52  		if opts.IgnoreDev && slices.Contains(mf.Groups()[mavenmanifest.MakeRequirementKey(req)], "test") {
    53  			// Skip the update if the dependency is of development group
    54  			// and updates on development dependencies are not desired
    55  			continue
    56  		}
    57  		if strings.Contains(req.Name, "${") || strings.Contains(req.Version, "${") {
    58  			// If there are unresolved properties, we should skip this version.
    59  			continue
    60  		}
    61  		if updated[req.VersionKey] {
    62  			// Skip the update if the dependency is already updated.
    63  			continue
    64  		}
    65  		updated[req.VersionKey] = true
    66  
    67  		latest, err := suggestMavenVersion(ctx, opts.ResolveClient, req, opts.UpgradeConfig.Get(req.Name))
    68  		if err != nil {
    69  			log.Warnf("failed to suggest Maven version for package %s: %v", req.Name, err)
    70  			continue
    71  		}
    72  		if latest.Version == req.Version {
    73  			// No need to update
    74  			continue
    75  		}
    76  
    77  		pu := result.PackageUpdate{
    78  			Name:        req.Name,
    79  			VersionFrom: req.Version,
    80  			VersionTo:   latest.Version,
    81  			Type:        req.Type,
    82  		}
    83  		origDep := mavenmanifest.OriginalDependency(pu, specific.LocalRequirements)
    84  		if origDep.Name() != ":" {
    85  			// An empty name indicates the dependency is not found, so the original dependency is not in the base project.
    86  			// Only add a package update if it is from the base project.
    87  			packageUpdates = append(packageUpdates, pu)
    88  		}
    89  	}
    90  
    91  	return result.Patch{PackageUpdates: packageUpdates}, nil
    92  }
    93  
    94  // suggestMavenVersion returns the latest version based on the given Maven requirement version.
    95  // If there is no newer version available, req will be returned.
    96  // For a version range requirement,
    97  //   - the greatest version matching the constraint is assumed when deciding whether the
    98  //     update is a major update or not.
    99  //   - if the latest version does not satisfy the constraint, this version is returned;
   100  //     otherwise, the original version range requirement is returned.
   101  func suggestMavenVersion(ctx context.Context, cl resolve.Client, req resolve.RequirementVersion, level upgrade.Level) (resolve.RequirementVersion, error) {
   102  	versions, err := cl.Versions(ctx, req.PackageKey)
   103  	if err != nil {
   104  		return resolve.RequirementVersion{}, fmt.Errorf("requesting versions of Maven package %s: %w", req.Name, err)
   105  	}
   106  	if len(versions) == 0 {
   107  		return resolve.RequirementVersion{}, fmt.Errorf("no versions found for Maven package %s", req.Name)
   108  	}
   109  
   110  	semvers := make([]*semver.Version, 0, len(versions))
   111  	for _, ver := range versions {
   112  		parsed, err := semver.Maven.Parse(ver.Version)
   113  		if err != nil {
   114  			log.Warnf("parsing Maven version %s: %v", parsed, err)
   115  			continue
   116  		}
   117  		semvers = append(semvers, parsed)
   118  	}
   119  
   120  	constraint, err := semver.Maven.ParseConstraint(req.Version)
   121  	if err != nil {
   122  		return resolve.RequirementVersion{}, fmt.Errorf("parsing Maven constraint %s: %w", req.Version, err)
   123  	}
   124  
   125  	var current *semver.Version
   126  	if constraint.IsSimple() {
   127  		// Constraint is a simple version string, so can be parsed to a single version.
   128  		current, err = semver.Maven.Parse(req.Version)
   129  		if err != nil {
   130  			return resolve.RequirementVersion{}, fmt.Errorf("parsing Maven version %s: %w", req.Version, err)
   131  		}
   132  	} else {
   133  		// Guess the latest version satisfying the constraint is being used
   134  		for _, v := range semvers {
   135  			if constraint.MatchVersion(v) && current.Compare(v) < 0 {
   136  				current = v
   137  			}
   138  		}
   139  	}
   140  
   141  	var newReq *semver.Version
   142  	for _, v := range semvers {
   143  		if mavenutil.CompareVersions(req.VersionKey, v, newReq) < 0 {
   144  			// Skip versions smaller than the current requirement
   145  			continue
   146  		}
   147  		if _, diff := v.Difference(current); !level.Allows(diff) {
   148  			continue
   149  		}
   150  		if mavenutil.IsPrerelease(v, req.VersionKey) {
   151  			// Skip prerelease versions for updates considering that most people prefer stable, released
   152  			// versions for dependency updates.
   153  			continue
   154  		}
   155  		newReq = v
   156  	}
   157  	if constraint.IsSimple() || !constraint.MatchVersion(newReq) {
   158  		// For version range requirement, update the requirement if the
   159  		// new requirement does not satisfy the constraint.
   160  		req.Version = newReq.String()
   161  	}
   162  
   163  	return req, nil
   164  }