Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only use version from plugin header. #93

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions features/dist-archive.feature
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,41 @@ Feature: Generate a distribution archive of a project
"""
And the {RUN_DIR}/subdir/hello-world-dist.zip file should exist
And the return code should be 0

Scenario: Uses version from plugin header when multiple versions are present
Given an empty directory
And a lifterlms/.distignore file:
"""
"""
And a lifterlms/lifterlms.php file:
"""
<?php
/**
* Main LifterLMS plugin file
*
* @version 5.3.0
*
* Plugin Name: LifterLMS
* Version: 7.6.0
*/
"""
And a lifterlms/class-lifterlms.php file:
"""
<?php
/**
* Main LifterLMS class
*
* @package LifterLMS/Main
*
* @since 1.0.0
* @version 7.2.0
*/
"""

When I run `wp dist-archive lifterlms`
Then STDOUT should be:
"""
Success: Created lifterlms.7.6.0.zip
"""
And STDERR should be empty
And the lifterlms.7.6.0.zip file should exist
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

<!-- For help understanding the `testVersion` configuration setting:
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
<config name="testVersion" value="5.6-"/>
<config name="testVersion" value="7.4-"/>

<!-- Verify that everything in the global namespace is either namespaced or prefixed.
See: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace -->
Expand Down
82 changes: 5 additions & 77 deletions src/Dist_Archive_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Dist_Archive_Command {
*/
private $checker;

private Version_Tool $version_tool;

/**
* Create a distribution archive based on a project's .distignore file.
*
Expand Down Expand Up @@ -69,6 +71,8 @@ class Dist_Archive_Command {
*/
public function __invoke( $args, $assoc_args ) {

$this->version_tool = new Version_Tool();

list( $source_dir_path, $destination_dir_path, $archive_file_name, $archive_output_dir_name ) = $this->get_file_paths_and_names( $args, $assoc_args );

$this->checker = new GitIgnoreChecker( $source_dir_path, '.distignore' );
Expand Down Expand Up @@ -241,7 +245,7 @@ private function get_file_paths_and_names( $args, $assoc_args ) {
: basename( $source_dir_path );

if ( is_null( $archive_file_name ) ) {
$version = $this->get_version( $source_dir_path );
$version = $this->version_tool->get_version( $source_dir_path );

// If the version number has been found, substitute it into the filename-format template, or just use the name.
$archive_file_stem = ! empty( $version )
Expand Down Expand Up @@ -326,82 +330,6 @@ private function maybe_create_directory( $destination_dir_path ) {
}
}

/**
* Gets the content of a version tag in any doc block in the given source code string.
*
* The version tag might be specified as "@version x.y.z" or "Version: x.y.z" and it can
* be preceded by an asterisk (*).
*
* @param string $code_str The source code string to look into.
* @return null|string The detected version string.
*/
private function get_version_in_code( $code_str ) {
$tokens = array_values(
array_filter(
token_get_all( $code_str ),
function ( $token ) {
return ! is_array( $token ) || T_WHITESPACE !== $token[0];
}
)
);
foreach ( $tokens as $token ) {
if ( T_DOC_COMMENT === $token[0] ) {
$version = $this->get_version_in_docblock( $token[1] );
if ( null !== $version ) {
return $version;
}
}
}
return null;
}

/**
* Gets the content of a version tag in a docblock.
*
* @param string $docblock Docblock to parse.
* @return null|string The content of the version tag.
*/
private function get_version_in_docblock( $docblock ) {
$docblocktags = $this->parse_doc_block( $docblock );
if ( isset( $docblocktags['version'] ) ) {
return $docblocktags['version'];
}
return null;
}

/**
* Parses a docblock and gets an array of tags with their values.
*
* The tags might be specified as "@version x.y.z" or "Version: x.y.z" and they can
* be preceded by an asterisk (*).
*
* This code is based on the 'phpactor' package.
* @see https://github.com/phpactor/docblock/blob/master/lib/Parser.php
*
* @param string $docblock Docblock to parse.
* @return array Associative array of parsed data.
*/
private function parse_doc_block( $docblock ) {
$tag_documentor = '{@([a-zA-Z0-9-_\\\]+)\s*?(.*)?}';
$tag_property = '{\s*\*?\s*(.*?):(.*)}';
$lines = explode( PHP_EOL, $docblock );
$tags = [];

foreach ( $lines as $line ) {
if ( 0 === preg_match( $tag_documentor, $line, $matches ) ) {
if ( 0 === preg_match( $tag_property, $line, $matches ) ) {
continue;
}
}

$tag_name = strtolower( $matches[1] );
$metadata = trim( isset( $matches[2] ) ? $matches[2] : '' );

$tags[ $tag_name ] = $metadata;
}
return $tags;
}

/**
* Run PHP's escapeshellcmd() then undo escaping known intentional characters.
*
Expand Down
119 changes: 119 additions & 0 deletions src/Version_Tool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

class Version_Tool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


public function get_version( string $path ): ?string {

$version = '';

/**
* If the path is a theme (meaning it contains a style.css file)
* parse the theme's version from the headers using a regex pattern.
* The pattern used is extracted from the get_file_data() function in core.
*
* @link https://developer.wordpress.org/reference/functions/get_file_data/
*/
if ( file_exists( $path . '/style.css' ) ) {
$contents = file_get_contents( $path . '/style.css', false, null, 0, 5000 );
$contents = str_replace( "\r", "\n", $contents );
$pattern = '/^' . preg_quote( 'Version', ',' ) . ':(.*)$/mi';
if ( preg_match( $pattern, $contents, $match ) && $match[1] ) {
$version = trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $match[1] ) );
}
}

if ( empty( $version ) ) {
foreach ( glob( $path . '/*.php' ) as $php_file ) {
$headers = $this->get_file_data(
$php_file,
array(
'name' => 'Plugin Name',
'version' => 'Version',
)
);
if ( empty( $headers['name'] ) ) {
continue;
}
if ( ! empty( $headers['version'] ) ) {
$version = $headers['version'];
break;
}
}
}

if ( empty( $version ) && file_exists( $path . '/composer.json' ) ) {
$composer_obj = json_decode( file_get_contents( $path . '/composer.json' ) );
if ( ! empty( $composer_obj->version ) ) {
$version = trim( $composer_obj->version );
}
}

if ( ! empty( $version ) && false !== stripos( $version, '-alpha' ) && is_dir( $path . '/.git' ) ) {
$response = WP_CLI::launch( "cd {$path}; git log --pretty=format:'%h' -n 1", false, true );
$maybe_hash = trim( $response->stdout );
if ( $maybe_hash && 7 === strlen( $maybe_hash ) ) {
$version .= '-' . $maybe_hash;
}
}

return $version;
}

/**
* Retrieves metadata from a file.
*
* Modified slightly from WordPress 6.5.2 wp-includes/functions.php:6830
* @see get_file_data()
* @see https://github.com/WordPress/WordPress/blob/ddc3f387b5df4687f5b829119d0c0f797be674bf/wp-includes/functions.php#L6830-L6888
*
* Searches for metadata in the first 8 KB of a file, such as a plugin or theme.
* Each piece of metadata must be on its own line. Fields can not span multiple
* lines, the value will get cut at the end of the first line.
*
* @link https://codex.wordpress.org/File_Header
*
* @param string $file Absolute path to the file.
* @param array $all_headers List of headers, in the format `array( 'HeaderKey' => 'Header Name' )`.
* @return string[] Array of file header values keyed by header name.
*/
private function get_file_data( string $file, array $all_headers ): array {

/**
* @see wp_initial_constants()
* `define( 'KB_IN_BYTES', 1024 );`
*/
$kb_in_bytes = 1024;

// Pull only the first 8 KB of the file in.
$file_data = file_get_contents( $file, false, null, 0, 8 * $kb_in_bytes );

if ( false === $file_data ) {
$file_data = '';
}

// Make sure we catch CR-only line endings.
$file_data = str_replace( "\r", "\n", $file_data );

/**
* Strips close comment and close php tags from file headers used by WP.
*
* functions.php:6763
*
* @param string $str Header comment to clean up.
* @return string
*/
$_cleanup_header_comment = function ( $str ) {
return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
};

foreach ( $all_headers as $field => $regex ) {
if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
$all_headers[ $field ] = $_cleanup_header_comment( $match[1] );
} else {
$all_headers[ $field ] = '';
}
}

return $all_headers;
}
}