diff --git a/src/Rules/SinceVersionRule.php b/src/Rules/SinceVersionRule.php index 80d21b5..a83f437 100644 --- a/src/Rules/SinceVersionRule.php +++ b/src/Rules/SinceVersionRule.php @@ -229,7 +229,60 @@ private static function getRequiresAtLeastValue( ?string $pluginFile ): string { return self::getRequiresAtLeastHeader( $path ); } - throw new \RuntimeException( 'No plugin or theme file found' ); + if ( is_string( $pluginFile ) ) { + throw new \RuntimeException( + sprintf( + 'WPCompat: The plugin file "%s" set in the WPCompat.pluginFile parameter was not found.', + $pluginFile + ) + ); + } + + // The directory name doesn't always match the main plugin file name, for example + // in a git worktree, so fall back to the same discovery that WordPress uses. + if ( is_string( $cwd ) ) { + $path = self::findPluginFile( $cwd ); + + if ( is_string( $path ) ) { + return self::getRequiresAtLeastHeader( $path ); + } + } + + throw new \RuntimeException( + sprintf( + 'WPCompat: Could not find a plugin or theme file in %s. Set the WPCompat.pluginFile or WPCompat.requiresAtLeast parameter in your PHPStan config file.', + is_string( $cwd ) ? $cwd : 'the current directory' + ) + ); + } + + /** + * The logic in this method matches the logic in WordPress core's get_plugins() function. + */ + private static function findPluginFile( string $dir ): ?string { + $files = glob( "{$dir}/*.php" ); + + if ( ! is_array( $files ) ) { + return null; + } + + sort( $files ); + + foreach ( $files as $file ) { + $contents = file_get_contents( $file, false, null, 0, 8 * 1024 ); + + if ( ! is_string( $contents ) ) { + continue; + } + + $contents = str_replace( "\r", "\n", $contents ); + + if ( preg_match( '/^[ \t\/*#@]*Plugin Name:(.*)$/mi', $contents, $match ) === 1 && trim( $match[1] ) !== '' ) { + return $file; + } + } + + return null; } /** diff --git a/tests/Unit/HeaderTest.php b/tests/Unit/HeaderTest.php index 699d019..728527d 100644 --- a/tests/Unit/HeaderTest.php +++ b/tests/Unit/HeaderTest.php @@ -57,4 +57,43 @@ public function dataMinimumVersions(): array { ], ]; } + + public function testPluginFileIsFoundWhenDirectoryNameDoesNotMatch(): void { + $since = $this->createRuleInDirectory( __DIR__ . '/data/worktree' ); + + self::assertSame( '5.5.0', $since->getMinVersion() ); + } + + public function testMissingPluginFileThrowsHelpfulException(): void { + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'Set the WPCompat.pluginFile or WPCompat.requiresAtLeast parameter' ); + + $this->createRuleInDirectory( __DIR__ . '/data/no-plugin' ); + } + + public function testMissingConfiguredPluginFileThrowsHelpfulException(): void { + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'The plugin file "does-not-exist.php" set in the WPCompat.pluginFile parameter was not found' ); + + new SinceVersionRule( + null, + 'does-not-exist.php', + self::$reflectionProvider + ); + } + + private function createRuleInDirectory( string $dir ): SinceVersionRule { + $cwd = (string) getcwd(); + chdir( $dir ); + + try { + return new SinceVersionRule( + null, + null, + self::$reflectionProvider + ); + } finally { + chdir( $cwd ); + } + } } diff --git a/tests/Unit/data/no-plugin/functions.php b/tests/Unit/data/no-plugin/functions.php new file mode 100644 index 0000000..61f0f4b --- /dev/null +++ b/tests/Unit/data/no-plugin/functions.php @@ -0,0 +1,4 @@ +