diff --git a/lib/class-importer.php b/lib/class-importer.php index c1c264e..4299f98 100644 --- a/lib/class-importer.php +++ b/lib/class-importer.php @@ -433,9 +433,12 @@ protected function import_class( array $data, $import_ignored = false ) { // Set class-specific meta update_post_meta( $class_id, '_wp-parser_final', (string) $data['final'] ); update_post_meta( $class_id, '_wp-parser_abstract', (string) $data['abstract'] ); - update_post_meta( $class_id, '_wp-parser_extends', $data['extends'] ); - update_post_meta( $class_id, '_wp-parser_implements', $data['implements'] ); - update_post_meta( $class_id, '_wp-parser_properties', $data['properties'] ); + // Metadata APIs unslash their input. map_deep() reaches every nested + // string, preserving the namespace separators in class relatives and + // property types. + update_post_meta( $class_id, '_wp-parser_extends', map_deep( $data['extends'], 'wp_slash' ) ); + update_post_meta( $class_id, '_wp-parser_implements', map_deep( $data['implements'], 'wp_slash' ) ); + update_post_meta( $class_id, '_wp-parser_properties', map_deep( $data['properties'], 'wp_slash' ) ); // Now add the methods foreach ( $data['methods'] as $method ) { @@ -743,13 +746,16 @@ public function import_item( array $data, $parent_post_id = 0, $import_ignored = $data['doc']['tags']['deprecated'] = $this->file_meta['deprecated']; } + // Metadata APIs unslash their input. map_deep() reaches every nested + // string, preserving the namespace separators in argument and alias + // types. if ( $post_data['post_type'] !== $this->post_type_class ) { - $anything_updated[] = update_post_meta( $post_id, '_wp-parser_args', $data['arguments'] ); + $anything_updated[] = update_post_meta( $post_id, '_wp-parser_args', map_deep( $data['arguments'], 'wp_slash' ) ); } // If the post type is using namespace aliases, record them. if ( ! empty( $data['aliases'] ) ) { - $anything_updated[] = update_post_meta( $post_id, '_wp_parser_aliases', (array) $data['aliases'] ); + $anything_updated[] = update_post_meta( $post_id, '_wp_parser_aliases', map_deep( (array) $data['aliases'], 'wp_slash' ) ); } // Recored the namespace if there is one. @@ -759,7 +765,11 @@ public function import_item( array $data, $parent_post_id = 0, $import_ignored = $anything_updated[] = update_post_meta( $post_id, '_wp-parser_line_num', (string) $data['line'] ); $anything_updated[] = update_post_meta( $post_id, '_wp-parser_end_line_num', (string) $data['end_line'] ); - $anything_updated[] = update_post_meta( $post_id, '_wp-parser_tags', $data['doc']['tags'] ); + + // Metadata APIs unslash their input. map_deep() reaches every nested + // string, preserving the namespace separators in tag types and + // references such as `@param \Foo\Bar` and `@see \Foo\Bar`. + $anything_updated[] = update_post_meta( $post_id, '_wp-parser_tags', map_deep( $data['doc']['tags'], 'wp_slash' ) ); // Metadata APIs unslash their input. map_deep() reaches retained JSON // objects as well as arrays, preserving backslashes in PHP and Blueprint diff --git a/tests/phpunit/tests/import/file.php b/tests/phpunit/tests/import/file.php index 2b55890..9eff2b3 100644 --- a/tests/phpunit/tests/import/file.php +++ b/tests/phpunit/tests/import/file.php @@ -213,4 +213,165 @@ public function test_function_snippet_metadata_preserves_backslashes() { $this->assertEquals( $snippets, get_post_meta( $post->ID, '_wp-parser_code_snippets', true ) ); $this->assertEquals( $setup_blueprints, get_post_meta( $post->ID, '_wp-parser_setup_blueprints', true ) ); } + + /** + * Test that WordPress metadata slashing does not alter DocBlock tags. + */ + public function test_function_tag_metadata_preserves_backslashes() { + + $posts = get_posts( + array( 'post_type' => $this->importer->post_type_function ) + ); + $post = $posts[0]; + + $function_data = $this->export_data['functions'][0]; + $tags = array( + array( + 'name' => 'see', + 'content' => '', + 'refers' => '\Docs\Example::method()', + ), + array( + 'name' => 'param', + 'content' => 'A namespaced parameter.', + 'types' => array( '\Foo', '\Foo\Bar', 'Vendor\Foo' ), + 'variable' => '$var', + ), + ); + + $function_data['doc']['tags'] = $tags; + + $this->importer->import_function( $function_data ); + + $this->assertEquals( $tags, get_post_meta( $post->ID, '_wp-parser_tags', true ) ); + } + + /** + * Test that WordPress metadata slashing does not alter argument metadata. + */ + public function test_function_argument_metadata_preserves_backslashes() { + + $posts = get_posts( + array( 'post_type' => $this->importer->post_type_function ) + ); + $post = $posts[0]; + + $function_data = $this->export_data['functions'][0]; + $arguments = array( + array( + 'name' => '$leading', + 'default' => null, + 'type' => '\Foo', + ), + array( + 'name' => '$qualified', + 'default' => null, + 'type' => '\Foo\Bar', + ), + array( + 'name' => '$relative', + 'default' => '\Vendor\Foo::DEFAULT_VALUE', + 'type' => 'Vendor\Foo', + ), + ); + + $function_data['arguments'] = $arguments; + + $this->importer->import_function( $function_data ); + + $this->assertEquals( $arguments, get_post_meta( $post->ID, '_wp-parser_args', true ) ); + } + + /** + * Test that WordPress metadata slashing does not alter namespace aliases. + */ + public function test_function_alias_metadata_preserves_backslashes() { + + $posts = get_posts( + array( 'post_type' => $this->importer->post_type_function ) + ); + $post = $posts[0]; + + $function_data = $this->export_data['functions'][0]; + $aliases = array( + 'Leading' => '\Foo', + 'Qualified' => '\Foo\Bar', + 'Relative' => 'Vendor\Foo', + ); + + $function_data['aliases'] = $aliases; + + $this->importer->import_function( $function_data ); + + $this->assertEquals( $aliases, get_post_meta( $post->ID, '_wp_parser_aliases', true ) ); + } + + /** + * Test that WordPress metadata slashing does not alter class metadata. + */ + public function test_class_metadata_preserves_backslashes() { + + $properties = array( + array( + 'name' => '$example', + 'line' => 12, + 'end_line' => 12, + 'default' => '\Vendor\Foo::DEFAULT_VALUE', + 'static' => false, + 'visibility' => 'public', + 'doc' => array( + 'description' => '', + 'long_description' => '', + 'tags' => array( + array( + 'name' => 'var', + 'content' => 'A namespaced property.', + 'types' => array( '\Foo', '\Foo\Bar', 'Vendor\Foo' ), + 'variable' => '', + ), + ), + ), + ), + ); + + $class_data = array( + 'name' => 'Slashing_Example', + 'namespace' => 'Vendor\Docs', + 'line' => 10, + 'end_line' => 14, + 'final' => false, + 'abstract' => false, + 'extends' => '\Foo\Bar', + 'implements' => array( '\Foo', 'Vendor\Foo' ), + 'properties' => $properties, + 'methods' => array(), + 'doc' => array( + 'description' => 'A class with namespaced relatives.', + 'long_description' => '', + 'tags' => array(), + ), + ); + + $file_data = $this->export_data; + $file_data['functions'] = array(); + $file_data['classes'] = array( $class_data ); + $file_data['hooks'] = array(); + + $this->importer->import_file( $file_data, true ); + + $posts = get_posts( + array( 'post_type' => $this->importer->post_type_class ) + ); + + $this->assertCount( 1, $posts ); + + $post = $posts[0]; + + $this->assertEquals( '\Foo\Bar', get_post_meta( $post->ID, '_wp-parser_extends', true ) ); + $this->assertEquals( array( '\Foo', 'Vendor\Foo' ), get_post_meta( $post->ID, '_wp-parser_implements', true ) ); + $this->assertEquals( $properties, get_post_meta( $post->ID, '_wp-parser_properties', true ) ); + + // The namespace is already compensated for; it must not be slashed twice. + $this->assertEquals( 'Vendor\Docs', get_post_meta( $post->ID, '_wp_parser_namespace', true ) ); + } }