diff --git a/README.md b/README.md
index 75ee9c34..639d9318 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,18 @@ Composer [blocks](https://getcomposer.org/doc/06-config.md#secure-http) you from
However, it's always advised to setup HTTPS to prevent MITM code injection.
+## Version constraints
+
+Typically, Composer best practices dictate that exact versions (e.g., 8.1.1) be avoided. However, using an inexact version constraint (e.g., ^8.1) can cause patching failures when the upstream package is updated. You may optionally display a warning regarding the use of inexact version constraints by using the following configuration in composer.json:
+
+```
+ "extra": {
+ "inexact-constraint-warning": true
+ }
+```
+
+This will be displayed during `composer update`.
+
## Error handling
If a patch cannot be applied (hunk failed, different line endings, etc.) a message will be shown and the patch will be skipped.
diff --git a/src/Patches.php b/src/Patches.php
index d1c1cf05..c5a1811b 100644
--- a/src/Patches.php
+++ b/src/Patches.php
@@ -15,12 +15,14 @@
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
+use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Installer\PackageEvents;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Installer\PackageEvent;
+use Composer\Semver\Constraint\MultiConstraint;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Symfony\Component\Process\Process;
@@ -272,6 +274,11 @@ public function postInstall(PackageEvent $event) {
}
$this->io->write(' - Applying patches for ' . $package_name . '');
+ $extra = $this->composer->getPackage()->getExtra();
+ if (!empty($extra['inexact-constraint-warning'])) {
+ $this->displayConstraintWarning($package);
+ }
+
// Get the install path from the package object.
$manager = $event->getComposer()->getInstallationManager();
$install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
@@ -287,6 +294,7 @@ public function postInstall(PackageEvent $event) {
foreach ($this->patches[$package_name] as $description => $url) {
$this->io->write(' ' . $url . ' (' . $description. ')');
+
try {
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
$this->getAndApplyPatch($downloader, $install_path, $url);
@@ -306,6 +314,30 @@ public function postInstall(PackageEvent $event) {
$this->writePatchReport($this->patches[$package_name], $install_path);
}
+ /**
+ * Displays a warning if the package's version constraint is inexact.
+ *
+ * @param Composer\Package $package
+ * The package for which to display the warning.
+ */
+ protected function displayConstraintWarning($package) {
+ // Gather all packages defined in root composer.json into a single array for version constraint access.
+ $root_requires = $this->composer->getPackage()->getRequires();
+ $root_dev_requires = $this->composer->getPackage()->getDevRequires();
+ $root_packages = array_merge($root_requires, $root_dev_requires);
+ $package_name = $package->getName();
+
+ if (!empty($root_packages[$package_name])) {
+ // If ^, ~, or * operators are being used, or this is a dev version without a hash specified, display warning.
+ /** @var MultiConstraint $link */
+ $link = $root_packages[$package_name]->getConstraint();
+ $version_constraint = $link->getPrettyString();
+ if (preg_match('/[\^~*]|(-dev)|(dev-)/', $version_constraint) && !strstr($version_constraint, '#')) {
+ $this->io->write(" $package_name has inexact version constraint. This may cause a patch failure now or in the future when the package is changed.");
+ }
+ }
+ }
+
/**
* Get a Package object from an OperationInterface object.
*