Skip to content
Draft
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
7 changes: 6 additions & 1 deletion lib/class-hook-reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ class Hook_Reflector extends BaseReflector {
* @return string
*/
public function getName() {
$printer = new \PhpParser\PrettyPrinter\Standard();
$name = $this->node->args[0]->value;
if ( $name instanceof \PhpParser\Node\Scalar\String_ ) {
return $name->value;
}

$printer = new Pretty_Printer();
return $this->cleanupName( $printer->prettyPrintExpr( $this->node->args[0]->value ) );
}

Expand Down
19 changes: 18 additions & 1 deletion lib/class-pretty-printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,24 @@
/**
* Extends default printer for arguments.
*/
class Pretty_Printer extends \PhpParser\PrettyPrinter\Standard {
class Pretty_Printer extends \phpDocumentor\Reflection\PrettyPrinter {
/**
* Print names as they appeared before PHP-Parser's name resolution.
*
* PHP-Parser represents resolved global names as fully-qualified names. The
* leading namespace separator is useful in an AST, but adding it to exported
* source expressions changes the established JSON output.
*
* @param \PhpParser\Node\Name\FullyQualified $node Fully-qualified name.
*
* @return string Printed name.
*/
protected function pName_FullyQualified( \PhpParser\Node\Name\FullyQualified $node ): string {
$name = $node->toString();

return false === strpos( $name, '\\' ) ? $name : '\\' . $name;
}

/**
* Pretty prints an argument.
*
Expand Down
139 changes: 99 additions & 40 deletions lib/runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function parse_files( $files, $root ) {
$out['constants'][] = array(
'name' => $constant->getShortName(),
'line' => $constant->getLineNumber(),
'value' => $constant->getValue(),
'value' => export_expression( $constant->getNode()->value ),
);
}

Expand All @@ -93,7 +93,7 @@ function parse_files( $files, $root ) {
$func = array(
'name' => $function->getShortName(),
'namespace' => $function->getNamespace(),
'aliases' => $function->getNamespaceAliases(),
'aliases' => strip_global_namespace_prefixes( $function->getNamespaceAliases() ),
'line' => $function->getLineNumber(),
'end_line' => $function->getNode()->getAttribute( 'endLine' ),
'arguments' => export_arguments( $function->getArguments() ),
Expand All @@ -120,8 +120,8 @@ function parse_files( $files, $root ) {
'end_line' => $class->getNode()->getAttribute( 'endLine' ),
'final' => $class->isFinal(),
'abstract' => $class->isAbstract(),
'extends' => $class->getParentClass(),
'implements' => $class->getInterfaces(),
'extends' => strip_global_namespace_prefix( $class->getParentClass() ),
'implements' => strip_global_namespace_prefixes( $class->getInterfaces() ),
'properties' => export_properties( $class->getProperties() ),
'methods' => export_methods( $class->getMethods() ),
'doc' => export_docblock( $class ),
Expand All @@ -137,33 +137,83 @@ function parse_files( $files, $root ) {
throw $e;
}

/*
* nikic/php-parser in version 3 started adding a namespace prefix
* at the start of global names, but this is different than how the
* documentation was previously generated. this removes those prefixes
* by removing a leading reverse solidus (\) when no other reverse
* solidus appears before the end of a sequence of PHP identifier
* characters.
*/
array_walk_recursive(
$output,
static function( &$value ) {
if ( is_string( $value ) ) {
// "\wp_kses()" -> "wp_kses()"
$without_global_namespace = preg_replace(
'~(^|\p{Z})\\\\([A-Z_a-z\x80-\xFF][0-9A-Z_a-z\x80-\xFF]*)([:(\p{Z}]|->|$)~',
'$1$2$3',
$value,
);
return $output;
}

if ( $value !== $without_global_namespace ) {
$value = $without_global_namespace;
}
}
}
/**
* Remove a synthetic leading namespace prefix from a global name.
*
* @param mixed $name Name to normalize.
*
* @return mixed
*/
function strip_global_namespace_prefix( $name ) {
if ( ! is_string( $name ) ) {
return $name;
}

return preg_replace(
'~^\\\\([A-Z_a-z\x80-\xFF][0-9A-Z_a-z\x80-\xFF]*)([:(\p{Z}]|->|$)~',
'$1$2',
$name
);
}

return $output;
/**
* Remove synthetic leading namespace prefixes from global names.
*
* @param array $names Names to normalize.
*
* @return array
*/
function strip_global_namespace_prefixes( array $names ) {
foreach ( $names as $key => $name ) {
$names[ $key ] = strip_global_namespace_prefix( $name );
}

return $names;
}

/**
* Export an expression without PHP-Parser's synthetic global namespace prefixes.
*
* @param null|\PhpParser\Node\Expr $expression Expression to export.
*
* @return null|string
*/
function export_expression( $expression ) {
if ( null === $expression ) {
return null;
}

static $printer = null;

if ( null === $printer ) {
$printer = new Pretty_Printer();
}

return $printer->prettyPrintExpr( $expression );
}

/**
* Remove synthetic global namespace prefixes from inline DocBlock references.
*
* @param string $text Formatted DocBlock text.
*
* @return string
*/
function strip_global_namespace_prefixes_from_inline_references( $text ) {
return preg_replace_callback(
'~{@(?:link|see)\s+([^}\s]+)~',
static function( $matches ) {
return str_replace(
$matches[1],
strip_global_namespace_prefix( $matches[1] ),
$matches[0]
);
},
$text
);
}

/**
Expand Down Expand Up @@ -237,27 +287,33 @@ function export_docblock( $element ) {
}

$output = array(
'description' => preg_replace( '/[\n\r]+/', ' ', $docblock->getShortDescription() ),
'long_description' => fix_newlines( $docblock->getLongDescription()->getFormattedContents() ),
'description' => strip_global_namespace_prefixes_from_inline_references(
preg_replace( '/[\n\r]+/', ' ', $docblock->getShortDescription() )
),
'long_description' => strip_global_namespace_prefixes_from_inline_references(
fix_newlines( $docblock->getLongDescription()->getFormattedContents() )
),
'tags' => array(),
);

foreach ( $docblock->getTags() as $tag ) {
$tag_data = array(
'name' => $tag->getName(),
'content' => preg_replace( '/[\n\r]+/', ' ', format_description( $tag->getDescription() ) ),
'content' => strip_global_namespace_prefixes_from_inline_references(
preg_replace( '/[\n\r]+/', ' ', format_description( $tag->getDescription() ) )
),
);
if ( method_exists( $tag, 'getTypes' ) ) {
$tag_data['types'] = $tag->getTypes();
$tag_data['types'] = strip_global_namespace_prefixes( $tag->getTypes() );
}
if ( method_exists( $tag, 'getLink' ) ) {
$tag_data['link'] = $tag->getLink();
$tag_data['link'] = strip_global_namespace_prefix( $tag->getLink() );
}
if ( method_exists( $tag, 'getVariableName' ) ) {
$tag_data['variable'] = $tag->getVariableName();
}
if ( method_exists( $tag, 'getReference' ) ) {
$tag_data['refers'] = $tag->getReference();
$tag_data['refers'] = strip_global_namespace_prefix( $tag->getReference() );
}
if ( method_exists( $tag, 'getVersion' ) ) {
// Version string.
Expand All @@ -267,7 +323,9 @@ function export_docblock( $element ) {
}
// Description string.
if ( method_exists( $tag, 'getDescription' ) ) {
$description = preg_replace( '/[\n\r]+/', ' ', format_description( $tag->getDescription() ) );
$description = strip_global_namespace_prefixes_from_inline_references(
preg_replace( '/[\n\r]+/', ' ', format_description( $tag->getDescription() ) )
);
if ( ! empty( $description ) ) {
$tag_data['description'] = $description;
}
Expand Down Expand Up @@ -312,8 +370,8 @@ function export_arguments( array $arguments ) {
foreach ( $arguments as $argument ) {
$output[] = array(
'name' => $argument->getName(),
'default' => $argument->getDefault(),
'type' => $argument->getType(),
'default' => export_expression( $argument->getNode()->default ),
'type' => strip_global_namespace_prefix( $argument->getType() ),
);
}

Expand All @@ -333,7 +391,7 @@ function export_properties( array $properties ) {
'name' => $property->getName(),
'line' => $property->getLineNumber(),
'end_line' => $property->getNode()->getAttribute( 'endLine' ),
'default' => $property->getDefault(),
'default' => export_expression( $property->getNode()->default ),
// 'final' => $property->isFinal(),
'static' => $property->isStatic(),
'visibility' => $property->getVisibility(),
Expand All @@ -357,7 +415,7 @@ function export_methods( array $methods ) {
$method_data = array(
'name' => $method->getShortName(),
'namespace' => $method->getNamespace(),
'aliases' => $method->getNamespaceAliases(),
'aliases' => strip_global_namespace_prefixes( $method->getNamespaceAliases() ),
'line' => $method->getLineNumber(),
'end_line' => $method->getNode()->getAttribute( 'endLine' ),
'final' => $method->isFinal(),
Expand Down Expand Up @@ -408,7 +466,7 @@ function export_uses( array $uses ) {
case 'methods':
$out[ $type ][] = array(
'name' => $name[1],
'class' => $name[0],
'class' => strip_global_namespace_prefix( $name[0] ),
'static' => $element->isStatic(),
'line' => $element->getLineNumber(),
'end_line' => $element->getNode()->getAttribute( 'endLine' ),
Expand All @@ -417,6 +475,7 @@ function export_uses( array $uses ) {

default:
case 'functions':
$name = strip_global_namespace_prefix( $name );
$out[ $type ][] = array(
'name' => $name,
'line' => $element->getLineNumber(),
Expand Down
46 changes: 44 additions & 2 deletions prep-diff.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ function wp_parser_prep_diff_is_simple_name_record_list( array $list ) {
* Normalizes scalar values that should not affect output comparisons.
*
* @param mixed $value Scalar value.
* @param array $path Current JSON path.
* @param string|null $key Parent object key.
* @return mixed Normalized value.
*/
function wp_parser_prep_diff_normalize_scalar( $value, $key ) {
function wp_parser_prep_diff_normalize_scalar( $value, array $path, $key ) {
if ( in_array( $key, array( 'line', 'end_line', 'startLine', 'endLine' ), true ) ) {
return 0;
}
Expand All @@ -91,6 +92,47 @@ function wp_parser_prep_diff_normalize_scalar( $value, $key ) {
return $value;
}

if ( in_array( $key, array( 'content', 'description', 'long_description' ), true ) ) {
return preg_replace_callback(
'~{@(?:link|see)\s+([^}\s]+)~',
static function( $matches ) {
return str_replace(
$matches[1],
wp_parser_prep_diff_normalize_global_names( $matches[1] ),
$matches[0]
);
},
$value
);
}

$parent_key = 1 < count( $path ) ? $path[ count( $path ) - 2 ] : null;
$normalize = in_array(
$key,
array( 'class', 'default', 'extends', 'link', 'refers', 'type', 'value' ),
true
);

$normalize = $normalize
|| in_array( $parent_key, array( 'aliases', 'implements', 'types' ), true )
|| (
'name' === $key
&& (
wp_parser_prep_diff_path_ends_with( $path, array( 'uses', 'functions', '[]', 'name' ) )
|| wp_parser_prep_diff_path_ends_with( $path, array( 'uses', 'methods', '[]', 'name' ) )
)
);

return $normalize ? wp_parser_prep_diff_normalize_global_names( $value ) : $value;
}

/**
* Remove synthetic global namespace prefixes from semantic names.
*
* @param string $value Name or expression to normalize.
* @return string Normalized value.
*/
function wp_parser_prep_diff_normalize_global_names( $value ) {
// "\wp_kses()" -> "wp_kses()".
$without_global_namespace = preg_replace(
'~(^|\p{Z})\\\\([A-Z_a-z\x80-\xFF][0-9A-Z_a-z\x80-\xFF]*)([:(\p{Z}]|->|$)~',
Expand Down Expand Up @@ -222,7 +264,7 @@ function wp_parser_prep_diff_should_sort_list( array $path, array $list ) {
*/
function wp_parser_prep_diff_normalize( $value, array $path = array(), $key = null ) {
if ( ! is_array( $value ) ) {
return wp_parser_prep_diff_normalize_scalar( $value, $key );
return wp_parser_prep_diff_normalize_scalar( $value, $path, $key );
}

if ( wp_parser_prep_diff_is_list( $value ) ) {
Expand Down
15 changes: 15 additions & 0 deletions tests/phpunit/tests/export/docblocks.inc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ function test_func( $var, $num ) {
return true;
}

/**
* Tests special characters in documentation.
*
* ```php
* true === wp_is_valid_utf8( '✏' );
* false === wp_is_valid_utf8( "just \xC0 test" );
* ```
*/
function test_special_characters() {}

/**
* \xC0 starts this description.
*/
function test_leading_escape_sequence() {}

/**
* This is a class docblock.
*
Expand Down
25 changes: 25 additions & 0 deletions tests/phpunit/tests/export/docblocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,31 @@ public function test_function_docblocks() {
);
}

/**
* Test that special characters in documentation are preserved.
*/
public function test_special_characters_are_preserved() {
$this->assertFunctionHasDocs(
'test_special_characters',
array(
'long_description' => '<pre><code class="language-php">true === wp_is_valid_utf8( \'✏\' );' . "\n"
. 'false === wp_is_valid_utf8( "just \\xC0 test" );</code></pre>',
)
);
}

/**
* Test that a leading escape sequence in documentation is preserved.
*/
public function test_leading_escape_sequence_is_preserved() {
$this->assertFunctionHasDocs(
'test_leading_escape_sequence',
array(
'description' => '\\xC0 starts this description.',
)
);
}

/**
* Test that class docs are exported.
*/
Expand Down
Loading
Loading