├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── lib ├── class-file-reflector.php ├── class-function-call-reflector.php ├── class-hook-reflector.php ├── class-method-call-reflector.php ├── class-static-method-call-reflector.php └── runner.php ├── phpunit.xml.dist └── tests ├── phpunit ├── includes │ ├── bootstrap.php │ └── export-testcase.php └── tests │ └── export │ ├── docblocks.inc │ ├── docblocks.php │ ├── hooks.inc │ ├── hooks.php │ ├── namespace.inc │ ├── namespace.php │ └── uses │ ├── constructor.inc │ ├── constructor.php │ ├── methods.inc │ ├── methods.php │ ├── nested.inc │ └── nested.php └── source ├── actions.php ├── bad-class-doc.php ├── class-property-doc.php ├── class_method_doc.php ├── deprecated-file.php ├── filters.php ├── functions.php ├── good-class.php └── relationships.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | matrix: 6 | include: 7 | - php: 7.1 8 | - php: 7.0 9 | - php: 5.6 10 | - php: 5.5 11 | - php: 5.4 12 | - php: hhvm 13 | 14 | before_install: 15 | # Setup WP_TESTS_DIR (needed to bootstrap WP PHPUnit tests). 16 | - export WP_TESTS_DIR=/tmp/wordpress/tests/phpunit/ 17 | # Clone the WordPress develop repo. 18 | - git clone --depth=1 --branch="4.3" git://develop.git.wordpress.org/ /tmp/wordpress/ 19 | # Setup DB. 20 | - mysql -e "CREATE DATABASE wordpress_test;" -uroot 21 | # Setup wp-config. 22 | - cp /tmp/wordpress/wp-tests-config-sample.php /tmp/wordpress/wp-tests-config.php 23 | - sed -i "s/youremptytestdbnamehere/wordpress_test/" /tmp/wordpress/wp-tests-config.php 24 | - sed -i "s/yourusernamehere/root/" /tmp/wordpress/wp-tests-config.php 25 | - sed -i "s/yourpasswordhere//" /tmp/wordpress/wp-tests-config.php 26 | 27 | install: composer install --no-dev 28 | 29 | script: 30 | - phpunit 31 | 32 | cache: 33 | directories: 34 | - vendor 35 | - $HOME/.composer/cache 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP Parser Lib 2 | 3 | This package contains the file scanning and hook parsing functionality from [WP Parser](https://github.com/WordPress/phpdoc-parser). 4 | 5 | I did not write this code. I just abstracted it so it can be used independently of the WP Parser WordPress plugin. 6 | 7 | ## Requirements 8 | * PHP 5.4+ 9 | * [Composer](https://getcomposer.org/) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | composer require wp-hooks/parser 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```php 20 | $files = \WP_Parser\get_wp_files( $path ); 21 | $output = \WP_Parser\parse_files( $files, $path ); 22 | ``` 23 | 24 | See [wp-hooks/generator](https://github.com/wp-hooks/generator) as a full usage example. 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "wp-hooks/parser", 3 | "description": "File scanning and hook parsing functionality from WP Parser.", 4 | "keywords" : ["wordpress"], 5 | "homepage" : "https://github.com/wp-hooks/parser", 6 | "license" : "GPL-2.0-or-later", 7 | "authors" : [ 8 | { 9 | "name" : "Ryan McCue", 10 | "homepage": "http://ryanmccue.info", 11 | "role" : "Developer" 12 | }, 13 | { 14 | "name" : "Contributors", 15 | "homepage": "https://github.com/wp-hooks/parser/graphs/contributors" 16 | } 17 | ], 18 | "support" : { 19 | "issues": "https://github.com/wp-hooks/parser/issues" 20 | }, 21 | "require" : { 22 | "php" : ">=5.4", 23 | "composer/installers" : "~1.0", 24 | "phpdocumentor/reflection" : "~3.0", 25 | "erusev/parsedown" : "~1.7" 26 | }, 27 | "autoload" : { 28 | "classmap": ["lib"], 29 | "files" : ["lib/runner.php"] 30 | }, 31 | "config": { 32 | "allow-plugins": { 33 | "composer/installers": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/class-file-reflector.php: -------------------------------------------------------------------------------- 1 | getType() ) { 70 | // Add classes, functions, and methods to the current location stack 71 | case 'Stmt_Class': 72 | case 'Stmt_Function': 73 | case 'Stmt_ClassMethod': 74 | array_push( $this->location, $node ); 75 | break; 76 | 77 | // Parse out hook definitions and function calls and add them to the queue. 78 | case 'Expr_FuncCall': 79 | $function = new Function_Call_Reflector( $node, $this->context ); 80 | 81 | // Add the call to the list of functions used in this scope. 82 | $this->getLocation()->uses['functions'][] = $function; 83 | 84 | if ( $this->isFilter( $node ) ) { 85 | if ( $this->last_doc && ! $node->getDocComment() ) { 86 | $node->setAttribute( 'comments', array( $this->last_doc ) ); 87 | $this->last_doc = null; 88 | } 89 | 90 | $hook = new Hook_Reflector( $node, $this->context ); 91 | 92 | // Add it to the list of hooks used in this scope. 93 | $this->getLocation()->uses['hooks'][] = $hook; 94 | } 95 | break; 96 | 97 | // Parse out method calls, so we can export where methods are used. 98 | case 'Expr_MethodCall': 99 | $method = new Method_Call_Reflector( $node, $this->context ); 100 | 101 | // Add it to the list of methods used in this scope. 102 | $this->getLocation()->uses['methods'][] = $method; 103 | break; 104 | 105 | // Parse out method calls, so we can export where methods are used. 106 | case 'Expr_StaticCall': 107 | $method = new Static_Method_Call_Reflector( $node, $this->context ); 108 | 109 | // Add it to the list of methods used in this scope. 110 | $this->getLocation()->uses['methods'][] = $method; 111 | break; 112 | 113 | // Parse out `new Class()` calls as uses of Class::__construct(). 114 | case 'Expr_New': 115 | $method = new \WP_Parser\Method_Call_Reflector( $node, $this->context ); 116 | 117 | // Add it to the list of methods used in this scope. 118 | $this->getLocation()->uses['methods'][] = $method; 119 | break; 120 | } 121 | 122 | // Pick up DocBlock from non-documentable elements so that it can be assigned 123 | // to the next hook if necessary. We don't do this for name nodes, since even 124 | // though they aren't documentable, they still carry the docblock from their 125 | // corresponding class/constant/function/etc. that they are the name of. If 126 | // we don't ignore them, we'll end up picking up docblocks that are already 127 | // associated with a named element, and so aren't really from a non- 128 | // documentable element after all. 129 | if ( ! $this->isNodeDocumentable( $node ) && 'Name' !== $node->getType() && ( $docblock = $node->getDocComment() ) ) { 130 | $this->last_doc = $docblock; 131 | } 132 | } 133 | 134 | /** 135 | * Assign queued hooks to functions and update the node stack on leaving a node. 136 | * 137 | * We can now access the function/method reflectors, so we can assign any queued 138 | * hooks to them. The reflector for a node isn't created until the node is left. 139 | * 140 | * @param \PHPParser_Node $node 141 | */ 142 | public function leaveNode( \PHPParser_Node $node ) { 143 | 144 | parent::leaveNode( $node ); 145 | 146 | switch ( $node->getType() ) { 147 | case 'Stmt_Class': 148 | $class = end( $this->classes ); 149 | if ( ! empty( $this->method_uses_queue ) ) { 150 | /** @var Reflection\ClassReflector\MethodReflector $method */ 151 | foreach ( $class->getMethods() as $method ) { 152 | if ( isset( $this->method_uses_queue[ $method->getName() ] ) ) { 153 | if ( isset( $this->method_uses_queue[ $method->getName() ]['methods'] ) ) { 154 | /* 155 | * For methods used in a class, set the class on the method call. 156 | * That allows us to later get the correct class name for $this, self, parent. 157 | */ 158 | foreach ( $this->method_uses_queue[ $method->getName() ]['methods'] as $method_call ) { 159 | /** @var Method_Call_Reflector $method_call */ 160 | $method_call->set_class( $class ); 161 | } 162 | } 163 | 164 | $method->uses = $this->method_uses_queue[ $method->getName() ]; 165 | } 166 | } 167 | } 168 | 169 | $this->method_uses_queue = array(); 170 | array_pop( $this->location ); 171 | break; 172 | 173 | case 'Stmt_Function': 174 | $function = array_pop( $this->location ); 175 | if ( isset( $function->uses ) && ! empty( $function->uses ) ) { 176 | end( $this->functions )->uses = $function->uses; 177 | } 178 | break; 179 | 180 | case 'Stmt_ClassMethod': 181 | $method = array_pop( $this->location ); 182 | 183 | /* 184 | * Store the list of elements used by this method in the queue. We'll 185 | * assign them to the method upon leaving the class (see above). 186 | */ 187 | if ( ! empty( $method->uses ) ) { 188 | $this->method_uses_queue[ $method->name ] = $method->uses; 189 | } 190 | break; 191 | } 192 | } 193 | 194 | /** 195 | * @param \PHPParser_Node $node 196 | * 197 | * @return bool 198 | */ 199 | protected function isFilter( \PHPParser_Node $node ) { 200 | // Ignore variable functions 201 | if ( 'Name' !== $node->name->getType() ) { 202 | return false; 203 | } 204 | 205 | $calling = (string) $node->name; 206 | 207 | $functions = array( 208 | 'apply_filters', 209 | 'apply_filters_ref_array', 210 | 'apply_filters_deprecated', 211 | 'do_action', 212 | 'do_action_ref_array', 213 | 'do_action_deprecated', 214 | ); 215 | 216 | return in_array( $calling, $functions ); 217 | } 218 | 219 | /** 220 | * @return File_Reflector 221 | */ 222 | protected function getLocation() { 223 | return empty( $this->location ) ? $this : end( $this->location ); 224 | } 225 | 226 | /** 227 | * @param \PHPParser_Node $node 228 | * 229 | * @return bool 230 | */ 231 | protected function isNodeDocumentable( \PHPParser_Node $node ) { 232 | return parent::isNodeDocumentable( $node ) 233 | || ( $node instanceof \PHPParser_Node_Expr_FuncCall 234 | && $this->isFilter( $node ) ); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /lib/class-function-call-reflector.php: -------------------------------------------------------------------------------- 1 | node->namespacedName ) ) { 23 | return '\\' . implode( '\\', $this->node->namespacedName->parts ); 24 | } 25 | 26 | $shortName = $this->getShortName(); 27 | 28 | if ( is_a( $shortName, 'PHPParser_Node_Name_FullyQualified' ) ) { 29 | return '\\' . (string) $shortName; 30 | } 31 | 32 | if ( is_a( $shortName, 'PHPParser_Node_Name' ) ) { 33 | return (string) $shortName; 34 | } 35 | 36 | /** @var \PHPParser_Node_Expr_ArrayDimFetch $shortName */ 37 | if ( is_a( $shortName, 'PHPParser_Node_Expr_ArrayDimFetch' ) ) { 38 | $var = $shortName->var->name; 39 | $dim = $shortName->dim->name->parts[0]; 40 | 41 | return "\${$var}[{$dim}]"; 42 | } 43 | 44 | /** @var \PHPParser_Node_Expr_Variable $shortName */ 45 | if ( is_a( $shortName, 'PHPParser_Node_Expr_Variable' ) ) { 46 | return $shortName->name; 47 | } 48 | 49 | return (string) $shortName; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/class-hook-reflector.php: -------------------------------------------------------------------------------- 1 | cleanupName( $printer->prettyPrintExpr( $this->node->args[0]->value ) ); 19 | } 20 | 21 | /** 22 | * @param string $name 23 | * 24 | * @return string 25 | */ 26 | private function cleanupName( $name ) { 27 | $matches = array(); 28 | 29 | // quotes on both ends of a string 30 | if ( preg_match( '/^[\'"]([^\'"]*)[\'"]$/', $name, $matches ) ) { 31 | return $matches[1]; 32 | } 33 | 34 | // two concatenated things, last one of them a variable 35 | if ( preg_match( 36 | '/(?:[\'"]([^\'"]*)[\'"]\s*\.\s*)?' . // First filter name string (optional) 37 | '(\$[^\s]*)' . // Dynamic variable 38 | '(?:\s*\.\s*[\'"]([^\'"]*)[\'"])?/', // Second filter name string (optional) 39 | $name, $matches ) ) { 40 | 41 | if ( isset( $matches[3] ) ) { 42 | return $matches[1] . '{' . $matches[2] . '}' . $matches[3]; 43 | } else { 44 | return $matches[1] . '{' . $matches[2] . '}'; 45 | } 46 | } 47 | 48 | return $name; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getShortName() { 55 | return $this->getName(); 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getType() { 62 | $type = 'filter'; 63 | switch ( (string) $this->node->name ) { 64 | case 'do_action': 65 | $type = 'action'; 66 | break; 67 | case 'do_action_ref_array': 68 | $type = 'action_reference'; 69 | break; 70 | case 'do_action_deprecated': 71 | $type = 'action_deprecated'; 72 | break; 73 | case 'apply_filters_ref_array': 74 | $type = 'filter_reference'; 75 | break; 76 | case 'apply_filters_deprecated'; 77 | $type = 'filter_deprecated'; 78 | break; 79 | } 80 | 81 | return $type; 82 | } 83 | 84 | /** 85 | * @return array 86 | */ 87 | public function getArgs() { 88 | $printer = new Pretty_Printer; 89 | $args = array(); 90 | foreach ( $this->node->args as $arg ) { 91 | $args[] = $printer->prettyPrintArg( $arg ); 92 | } 93 | 94 | // Skip the filter name 95 | array_shift( $args ); 96 | 97 | return $args; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/class-method-call-reflector.php: -------------------------------------------------------------------------------- 1 | node->getType() ) { 28 | $name = '__construct'; 29 | $caller = $this->node->class; 30 | } else { 31 | $name = $this->getShortName(); 32 | $caller = $this->node->var; 33 | } 34 | 35 | if ( $caller instanceof \PHPParser_Node_Expr ) { 36 | $printer = new Pretty_Printer; 37 | $caller = $printer->prettyPrintExpr( $caller ); 38 | } elseif ( $caller instanceof \PHPParser_Node_Name_FullyQualified ) { 39 | $caller = '\\' . $caller->toString(); 40 | } elseif ( $caller instanceof \PHPParser_Node_Name ) { 41 | $caller = $caller->toString(); 42 | } 43 | 44 | $caller = $this->_resolveName( $caller ); 45 | 46 | // If the caller is a function, convert it to the function name 47 | if ( is_a( $caller, 'PHPParser_Node_Expr_FuncCall' ) ) { 48 | 49 | // Add parentheses to signify this is a function call 50 | /** @var \PHPParser_Node_Expr_FuncCall $caller */ 51 | $caller = implode( '\\', $caller->name->parts ) . '()'; 52 | } 53 | 54 | $class_mapping = $this->_getClassMapping(); 55 | if ( array_key_exists( $caller, $class_mapping ) ) { 56 | $caller = $class_mapping[ $caller ]; 57 | } 58 | 59 | return array( $caller, $name ); 60 | } 61 | 62 | /** 63 | * Set the class that this method was called within. 64 | * 65 | * @param ClassReflector $class 66 | */ 67 | public function set_class( ClassReflector $class ) { 68 | 69 | $this->called_in_class = $class; 70 | } 71 | 72 | /** 73 | * Returns whether or not this method call is a static call 74 | * 75 | * @return bool Whether or not this method call is a static call 76 | */ 77 | public function isStatic() { 78 | return false; 79 | } 80 | 81 | /** 82 | * Returns a mapping from variable names to a class name, leverages globals for most used classes 83 | * 84 | * @return array Class mapping to map variable names to classes 85 | */ 86 | protected function _getClassMapping() { 87 | 88 | // List of global use generated using following command: 89 | // ack "global \\\$[^;]+;" --no-filename | tr -d '\t' | sort | uniq | sed "s/global //g" | sed "s/, /,/g" | tr , '\n' | sed "s/;//g" | sort | uniq | sed "s/\\\$//g" | sed "s/[^ ][^ ]*/'&' => ''/g" 90 | // There is probably an easier way, there are currently no globals that are classes starting with an underscore 91 | $wp_globals = array( 92 | 'authordata' => 'WP_User', 93 | 'custom_background' => 'Custom_Background', 94 | 'custom_image_header' => 'Custom_Image_Header', 95 | 'phpmailer' => 'PHPMailer', 96 | 'post' => 'WP_Post', 97 | 'userdata' => 'WP_User', // This can also be stdClass, but you can't call methods on an stdClass 98 | 'wp' => 'WP', 99 | 'wp_admin_bar' => 'WP_Admin_Bar', 100 | 'wp_customize' => 'WP_Customize_Manager', 101 | 'wp_embed' => 'WP_Embed', 102 | 'wp_filesystem' => 'WP_Filesystem', 103 | 'wp_hasher' => 'PasswordHash', // This can be overridden by plugins, for core assume this is ours 104 | 'wp_json' => 'Services_JSON', 105 | 'wp_list_table' => 'WP_List_Table', // This one differs because there are a lot of different List Tables, assume they all only overwrite existing functions on WP_List_Table 106 | 'wp_locale' => 'WP_Locale', 107 | 'wp_object_cache' => 'WP_Object_Cache', 108 | 'wp_query' => 'WP_Query', 109 | 'wp_rewrite' => 'WP_Rewrite', 110 | 'wp_roles' => 'WP_Roles', 111 | 'wp_scripts' => 'WP_Scripts', 112 | 'wp_styles' => 'WP_Styles', 113 | 'wp_the_query' => 'WP_Query', 114 | 'wp_widget_factory' => 'WP_Widget_Factory', 115 | 'wp_xmlrpc_server' => 'wp_xmlrpc_server', // This can be overridden by plugins, for core assume this is ours 116 | 'wpdb' => 'wpdb', 117 | ); 118 | 119 | $wp_functions = array( 120 | 'get_current_screen()' => 'WP_Screen', 121 | '_get_list_table()' => 'WP_List_Table', // This one differs because there are a lot of different List Tables, assume they all only overwrite existing functions on WP_List_Table 122 | 'wp_get_theme()' => 'WP_Theme', 123 | ); 124 | 125 | $class_mapping = array_merge( $wp_globals, $wp_functions ); 126 | 127 | return $class_mapping; 128 | } 129 | 130 | /** 131 | * Resolve a class name from self/parent. 132 | * 133 | * @param string $class The class name. 134 | * 135 | * @return string The resolved class name. 136 | */ 137 | protected function _resolveName( $class ) { 138 | 139 | if ( ! $this->called_in_class ) { 140 | return $class; 141 | } 142 | 143 | 144 | switch ( $class ) { 145 | case '$this': 146 | case 'self': 147 | $namespace = (string) $this->called_in_class->getNamespace(); 148 | $namespace = ( 'global' !== $namespace ) ? $namespace . '\\' : ''; 149 | $class = '\\' . $namespace . $this->called_in_class->getShortName(); 150 | break; 151 | case 'parent': 152 | $class = '\\' . $this->called_in_class->getNode()->extends->toString(); 153 | break; 154 | } 155 | 156 | return $class; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/class-static-method-call-reflector.php: -------------------------------------------------------------------------------- 1 | node->class; 17 | $prefix = ( is_a( $class, 'PHPParser_Node_Name_FullyQualified' ) ) ? '\\' : ''; 18 | $class = $prefix . $this->_resolveName( implode( '\\', $class->parts ) ); 19 | 20 | return array( $class, $this->getShortName() ); 21 | } 22 | 23 | /** 24 | * @return bool 25 | */ 26 | public function isStatic() { 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/runner.php: -------------------------------------------------------------------------------- 1 | getExtension() ) { 26 | continue; 27 | } 28 | 29 | $files[] = $file->getPathname(); 30 | } 31 | } catch ( \UnexpectedValueException $exc ) { 32 | return new \WP_Error( 33 | 'unexpected_value_exception', 34 | sprintf( 'Directory [%s] contained a directory we can not recurse into', $directory ) 35 | ); 36 | } 37 | 38 | return $files; 39 | } 40 | 41 | /** 42 | * @param array $files 43 | * @param string $root 44 | * 45 | * @return array 46 | */ 47 | function parse_files( $files, $root ) { 48 | $output = array(); 49 | 50 | foreach ( $files as $filename ) { 51 | $file = new File_Reflector( $filename ); 52 | 53 | $path = ltrim( substr( $filename, strlen( $root ) ), DIRECTORY_SEPARATOR ); 54 | $file->setFilename( $path ); 55 | 56 | $file->process(); 57 | 58 | // TODO proper exporter 59 | $out = array( 60 | 'file' => export_docblock( $file ), 61 | 'path' => str_replace( DIRECTORY_SEPARATOR, '/', $file->getFilename() ), 62 | 'root' => $root, 63 | ); 64 | 65 | if ( ! empty( $file->uses ) ) { 66 | $out['uses'] = export_uses( $file->uses ); 67 | } 68 | 69 | foreach ( $file->getIncludes() as $include ) { 70 | $out['includes'][] = array( 71 | 'name' => $include->getName(), 72 | 'line' => $include->getLineNumber(), 73 | 'type' => $include->getType(), 74 | ); 75 | } 76 | 77 | foreach ( $file->getConstants() as $constant ) { 78 | $out['constants'][] = array( 79 | 'name' => $constant->getShortName(), 80 | 'line' => $constant->getLineNumber(), 81 | 'value' => $constant->getValue(), 82 | ); 83 | } 84 | 85 | if ( ! empty( $file->uses['hooks'] ) ) { 86 | $out['hooks'] = export_hooks( $file->uses['hooks'] ); 87 | } 88 | 89 | foreach ( $file->getFunctions() as $function ) { 90 | $func = array( 91 | 'name' => $function->getShortName(), 92 | 'namespace' => $function->getNamespace(), 93 | 'aliases' => $function->getNamespaceAliases(), 94 | 'line' => $function->getLineNumber(), 95 | 'end_line' => $function->getNode()->getAttribute( 'endLine' ), 96 | 'arguments' => export_arguments( $function->getArguments() ), 97 | 'doc' => export_docblock( $function ), 98 | 'hooks' => array(), 99 | ); 100 | 101 | if ( ! empty( $function->uses ) ) { 102 | $func['uses'] = export_uses( $function->uses ); 103 | 104 | if ( ! empty( $function->uses['hooks'] ) ) { 105 | $func['hooks'] = export_hooks( $function->uses['hooks'] ); 106 | } 107 | } 108 | 109 | $out['functions'][] = $func; 110 | } 111 | 112 | foreach ( $file->getClasses() as $class ) { 113 | $class_data = array( 114 | 'name' => $class->getShortName(), 115 | 'namespace' => $class->getNamespace(), 116 | 'line' => $class->getLineNumber(), 117 | 'end_line' => $class->getNode()->getAttribute( 'endLine' ), 118 | 'final' => $class->isFinal(), 119 | 'abstract' => $class->isAbstract(), 120 | 'extends' => $class->getParentClass(), 121 | 'implements' => $class->getInterfaces(), 122 | 'properties' => export_properties( $class->getProperties() ), 123 | 'methods' => export_methods( $class->getMethods() ), 124 | 'doc' => export_docblock( $class ), 125 | ); 126 | 127 | $out['classes'][] = $class_data; 128 | } 129 | 130 | $output[] = $out; 131 | } 132 | 133 | return $output; 134 | } 135 | 136 | /** 137 | * Fixes newline handling in parsed text. 138 | * 139 | * DocBlock lines, particularly for descriptions, generally adhere to a given character width. For sentences and 140 | * paragraphs that exceed that width, what is intended as a manual soft wrap (via line break) is used to ensure 141 | * on-screen/in-file legibility of that text. These line breaks are retained by phpDocumentor. However, consumers 142 | * of this parsed data may believe the line breaks to be intentional and may display the text as such. 143 | * 144 | * This function fixes text by merging consecutive lines of text into a single line. A special exception is made 145 | * for text appearing in `` and `
` tags, as newlines appearing in those tags are always intentional.
146 |  *
147 |  * @param string $text
148 |  *
149 |  * @return string
150 |  */
151 | function fix_newlines( $text ) {
152 | 	// Non-naturally occurring string to use as temporary replacement.
153 | 	$replacement_string = '{{{{{}}}}}';
154 | 
155 | 	// Replace newline characters within 'code' and 'pre' tags with replacement string.
156 | 	$text = preg_replace_callback(
157 | 		"/(?<=
)(.+)(?=<\/code><\/pre>)/s",
158 | 		function ( $matches ) use ( $replacement_string ) {
159 | 			return preg_replace( '/[\n\r]/', $replacement_string, $matches[1] );
160 | 		},
161 | 		$text
162 | 	);
163 | 
164 | 	// Merge consecutive non-blank lines together by replacing the newlines with a space.
165 | 	$text = preg_replace(
166 | 		"/[\n\r](?!\s*[\n\r])/m",
167 | 		' ',
168 | 		$text
169 | 	);
170 | 
171 | 	// Restore newline characters into code blocks.
172 | 	$text = str_replace( $replacement_string, "\n", $text );
173 | 
174 | 	return $text;
175 | }
176 | 
177 | /**
178 |  * @param BaseReflector|ReflectionAbstract $element
179 |  *
180 |  * @return array
181 |  */
182 | function export_docblock( $element ) {
183 | 	$docblock = $element->getDocBlock();
184 | 	if ( ! $docblock ) {
185 | 		return array(
186 | 			'description'      => '',
187 | 			'long_description' => '',
188 | 			'tags'             => array(),
189 | 		);
190 | 	}
191 | 
192 | 	$output = array(
193 | 		'description'      => preg_replace( '/[\n\r]+/', ' ', $docblock->getShortDescription() ),
194 | 		'long_description' => fix_newlines( $docblock->getLongDescription()->getFormattedContents() ),
195 | 		'tags'             => array(),
196 | 	);
197 | 
198 | 	foreach ( $docblock->getTags() as $tag ) {
199 | 		$tag_data = array(
200 | 			'name'    => $tag->getName(),
201 | 			'content' => preg_replace( '/[\n\r]+/', ' ', format_description( $tag->getDescription() ) ),
202 | 		);
203 | 		if ( method_exists( $tag, 'getTypes' ) ) {
204 | 			$tag_data['types'] = $tag->getTypes();
205 | 		}
206 | 		if ( method_exists( $tag, 'getLink' ) ) {
207 | 			$tag_data['link'] = $tag->getLink();
208 | 		}
209 | 		if ( method_exists( $tag, 'getVariableName' ) ) {
210 | 			$tag_data['variable'] = $tag->getVariableName();
211 | 		}
212 | 		if ( method_exists( $tag, 'getReference' ) ) {
213 | 			$tag_data['refers'] = $tag->getReference();
214 | 		}
215 | 		if ( method_exists( $tag, 'getVersion' ) ) {
216 | 			// Version string.
217 | 			$version = $tag->getVersion();
218 | 			if ( ! empty( $version ) ) {
219 | 				$tag_data['content'] = $version;
220 | 			}
221 | 			// Description string.
222 | 			if ( method_exists( $tag, 'getDescription' ) ) {
223 | 				$description = preg_replace( '/[\n\r]+/', ' ', format_description( $tag->getDescription() ) );
224 | 				if ( ! empty( $description ) ) {
225 | 					$tag_data['description'] = $description;
226 | 				}
227 | 			}
228 | 		}
229 | 		$output['tags'][] = $tag_data;
230 | 	}
231 | 
232 | 	return $output;
233 | }
234 | 
235 | /**
236 |  * @param Hook_Reflector[] $hooks
237 |  *
238 |  * @return array
239 |  */
240 | function export_hooks( array $hooks ) {
241 | 	$out = array();
242 | 
243 | 	foreach ( $hooks as $hook ) {
244 | 		$out[] = array(
245 | 			'name'      => $hook->getName(),
246 | 			'line'      => $hook->getLineNumber(),
247 | 			'end_line'  => $hook->getNode()->getAttribute( 'endLine' ),
248 | 			'type'      => $hook->getType(),
249 | 			'arguments' => $hook->getArgs(),
250 | 			'doc'       => export_docblock( $hook ),
251 | 		);
252 | 	}
253 | 
254 | 	return $out;
255 | }
256 | 
257 | /**
258 |  * @param ArgumentReflector[] $arguments
259 |  *
260 |  * @return array
261 |  */
262 | function export_arguments( array $arguments ) {
263 | 	$output = array();
264 | 
265 | 	foreach ( $arguments as $argument ) {
266 | 		$output[] = array(
267 | 			'name'    => $argument->getName(),
268 | 			'default' => $argument->getDefault(),
269 | 			'type'    => $argument->getType(),
270 | 		);
271 | 	}
272 | 
273 | 	return $output;
274 | }
275 | 
276 | /**
277 |  * @param PropertyReflector[] $properties
278 |  *
279 |  * @return array
280 |  */
281 | function export_properties( array $properties ) {
282 | 	$out = array();
283 | 
284 | 	foreach ( $properties as $property ) {
285 | 		$out[] = array(
286 | 			'name'        => $property->getName(),
287 | 			'line'        => $property->getLineNumber(),
288 | 			'end_line'    => $property->getNode()->getAttribute( 'endLine' ),
289 | 			'default'     => $property->getDefault(),
290 | //			'final' => $property->isFinal(),
291 | 			'static'      => $property->isStatic(),
292 | 			'visibility'  => $property->getVisibility(),
293 | 			'doc'         => export_docblock( $property ),
294 | 		);
295 | 	}
296 | 
297 | 	return $out;
298 | }
299 | 
300 | /**
301 |  * @param MethodReflector[] $methods
302 |  *
303 |  * @return array
304 |  */
305 | function export_methods( array $methods ) {
306 | 	$output = array();
307 | 
308 | 	foreach ( $methods as $method ) {
309 | 
310 | 		$method_data = array(
311 | 			'name'       => $method->getShortName(),
312 | 			'namespace'  => $method->getNamespace(),
313 | 			'aliases'    => $method->getNamespaceAliases(),
314 | 			'line'       => $method->getLineNumber(),
315 | 			'end_line'   => $method->getNode()->getAttribute( 'endLine' ),
316 | 			'final'      => $method->isFinal(),
317 | 			'abstract'   => $method->isAbstract(),
318 | 			'static'     => $method->isStatic(),
319 | 			'visibility' => $method->getVisibility(),
320 | 			'arguments'  => export_arguments( $method->getArguments() ),
321 | 			'doc'        => export_docblock( $method ),
322 | 		);
323 | 
324 | 		if ( ! empty( $method->uses ) ) {
325 | 			$method_data['uses'] = export_uses( $method->uses );
326 | 
327 | 			if ( ! empty( $method->uses['hooks'] ) ) {
328 | 				$method_data['hooks'] = export_hooks( $method->uses['hooks'] );
329 | 			}
330 | 		}
331 | 
332 | 		$output[] = $method_data;
333 | 	}
334 | 
335 | 	return $output;
336 | }
337 | 
338 | /**
339 |  * Export the list of elements used by a file or structure.
340 |  *
341 |  * @param array $uses {
342 |  *        @type Function_Call_Reflector[] $functions The functions called.
343 |  * }
344 |  *
345 |  * @return array
346 |  */
347 | function export_uses( array $uses ) {
348 | 	$out = array();
349 | 
350 | 	// Ignore hooks here, they are exported separately.
351 | 	unset( $uses['hooks'] );
352 | 
353 | 	foreach ( $uses as $type => $used_elements ) {
354 | 
355 | 		/** @var MethodReflector|FunctionReflector $element */
356 | 		foreach ( $used_elements as $element ) {
357 | 
358 | 			$name = $element->getName();
359 | 
360 | 			switch ( $type ) {
361 | 				case 'methods':
362 | 					$out[ $type ][] = array(
363 | 						'name'     => $name[1],
364 | 						'class'    => $name[0],
365 | 						'static'   => $element->isStatic(),
366 | 						'line'     => $element->getLineNumber(),
367 | 						'end_line' => $element->getNode()->getAttribute( 'endLine' ),
368 | 					);
369 | 					break;
370 | 
371 | 				default:
372 | 				case 'functions':
373 | 					$out[ $type ][] = array(
374 | 						'name'     => $name,
375 | 						'line'     => $element->getLineNumber(),
376 | 						'end_line' => $element->getNode()->getAttribute( 'endLine' ),
377 | 					);
378 | 
379 | 					if ( '_deprecated_file' === $name
380 | 						|| '_deprecated_function' === $name
381 | 						|| '_deprecated_argument' === $name
382 | 						|| '_deprecated_hook' === $name
383 | 					) {
384 | 						$arguments = $element->getNode()->args;
385 | 
386 | 						$out[ $type ][0]['deprecation_version'] = $arguments[1]->value->value;
387 | 					}
388 | 
389 | 					break;
390 | 			}
391 | 		}
392 | 	}
393 | 
394 | 	return $out;
395 | }
396 | 
397 | /**
398 |  * Format the given description with Markdown.
399 |  *
400 |  * @param string $description Description.
401 |  * @return string Description as Markdown if the Parsedown class exists, otherwise return
402 |  *                the given description text.
403 |  */
404 | function format_description( $description ) {
405 | 	if ( class_exists( 'Parsedown' ) ) {
406 | 		$parsedown   = \Parsedown::instance();
407 | 		$description = $parsedown->line( $description );
408 | 	}
409 | 	return $description;
410 | }
411 | 


--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
 1 | 
 6 | 	
 7 | 		
 8 | 			tests/phpunit/tests/
 9 | 		
10 | 	
11 | 
12 | 


--------------------------------------------------------------------------------
/tests/phpunit/includes/bootstrap.php:
--------------------------------------------------------------------------------
 1 | getFileName();
 28 | 		$file = rtrim( $file, 'php' ) . 'inc';
 29 | 		$path = dirname( $file );
 30 | 
 31 | 		$export_data = \WP_Parser\parse_files( array( $file ), $path );
 32 | 
 33 | 		$this->export_data = $export_data[0];
 34 | 	}
 35 | 
 36 | 	/**
 37 | 	 * Parse the file to get the exported data before the first test.
 38 | 	 */
 39 | 	public function setUp() {
 40 | 
 41 | 		parent::setUp();
 42 | 
 43 | 		if ( ! $this->export_data ) {
 44 | 			$this->parse_file();
 45 | 		}
 46 | 	}
 47 | 
 48 | 	/**
 49 | 	 * Assert that an entity contains another entity.
 50 | 	 *
 51 | 	 * @param array  $entity   The exported entity data.
 52 | 	 * @param string $type     The type of thing that this entity should contain.
 53 | 	 * @param array  $expected The expected data for the thing the entity should contain.
 54 | 	 */
 55 | 	protected function assertEntityContains( $entity, $type, $expected ) {
 56 | 
 57 | 		$this->assertArrayHasKey( $type, $entity );
 58 | 
 59 | 		foreach ( $entity[ $type ] as $exported ) {
 60 | 			if ( $exported['line'] == $expected['line'] ) {
 61 | 				foreach ( $expected as $key => $expected_value ) {
 62 | 					$this->assertEquals( $expected_value, $exported[ $key ] );
 63 | 				}
 64 | 
 65 | 				return;
 66 | 			}
 67 | 		}
 68 | 
 69 | 		$this->fail( "No matching {$type} contained by {$entity['name']}." );
 70 | 	}
 71 | 
 72 | 	/**
 73 | 	 * Assert that a file contains the declaration of a hook.
 74 | 	 *
 75 | 	 * @param array $hook The expected export data for the hook.
 76 | 	 */
 77 | 	protected function assertFileContainsHook( $hook ) {
 78 | 
 79 | 		$this->assertEntityContains( $this->export_data, 'hooks', $hook );
 80 | 	}
 81 | 
 82 | 	/**
 83 | 	 * Assert that an entity uses another entity.
 84 | 	 *
 85 | 	 * @param array  $entity The exported entity data.
 86 | 	 * @param string $type   The type of thing that this entity should use.
 87 | 	 * @param array  $used   The expected data for the thing the entity should use.
 88 | 	 */
 89 | 	protected function assertEntityUses( $entity, $type, $used ) {
 90 | 
 91 | 		if ( ! $this->entity_uses( $entity, $type, $used ) ) {
 92 | 
 93 | 			$name = isset( $entity['path'] ) ? $entity['path'] : $entity['name'];
 94 | 
 95 | 			$this->fail( "No matching {$type} used by {$name}." );
 96 | 		}
 97 | 	}
 98 | 
 99 | 	/**
100 | 	 * Assert that an entity doesn't use another entity.
101 | 	 *
102 | 	 * @param array  $entity The exported entity data.
103 | 	 * @param string $type   The type of thing that this entity shouldn't use.
104 | 	 * @param array  $used   The expected data for the thing the entity shouldn't use.
105 | 	 */
106 | 	protected function assertEntityNotUses( $entity, $type, $used ) {
107 | 
108 | 		if ( $this->entity_uses( $entity, $type, $used ) ) {
109 | 
110 | 			$name = isset( $entity['path'] ) ? $entity['path'] : $entity['name'];
111 | 
112 | 			$this->fail( "Matching {$type} used by {$name}." );
113 | 		}
114 | 	}
115 | 
116 | 	/**
117 | 	 * Assert that a function uses another entity.
118 | 	 *
119 | 	 * @param string $type          The type of entity. E.g. 'functions', 'methods'.
120 | 	 * @param string $function_name The name of the function that uses this function.
121 | 	 * @param array  $entity        The expected exported data for the used entity.
122 | 	 */
123 | 	protected function assertFunctionUses( $type, $function_name, $entity ) {
124 | 
125 | 		$function_data = $this->find_entity_data_in(
126 | 			$this->export_data
127 | 			, 'functions'
128 | 			, $function_name
129 | 		);
130 | 
131 | 		$this->assertInternalType( 'array', $function_data );
132 | 		$this->assertEntityUses( $function_data, $type, $entity );
133 | 	}
134 | 
135 | 	/**
136 | 	 * Assert that a function doesn't use another entity.
137 | 	 *
138 | 	 * @param string $type          The type of entity. E.g. 'functions', 'methods'.
139 | 	 * @param string $function_name The name of the function that uses this function.
140 | 	 * @param array  $entity        The expected exported data for the used entity.
141 | 	 */
142 | 	protected function assertFunctionNotUses( $type, $function_name, $entity ) {
143 | 
144 | 		$function_data = $this->find_entity_data_in(
145 | 			$this->export_data
146 | 			, 'functions'
147 | 			, $function_name
148 | 		);
149 | 
150 | 		$this->assertInternalType( 'array', $function_data );
151 | 		$this->assertEntityNotUses( $function_data, $type, $entity );
152 | 	}
153 | 
154 | 	/**
155 | 	 * Assert that a method uses another entity.
156 | 	 *
157 | 	 * @param string $type        The type of entity. E.g. 'functions', 'methods'.
158 | 	 * @param string $class_name  The name of the class that the method is used in.
159 | 	 * @param string $method_name The name of the method that uses this method.
160 | 	 * @param array  $entity      The expected exported data for this entity.
161 | 	 */
162 | 	protected function assertMethodUses( $type, $class_name, $method_name, $entity ) {
163 | 
164 | 		$class_data = $this->find_entity_data_in(
165 | 			$this->export_data
166 | 			, 'classes'
167 | 			, $class_name
168 | 		);
169 | 
170 | 		$this->assertInternalType( 'array', $class_data );
171 | 
172 | 		$method_data = $this->find_entity_data_in(
173 | 			$class_data
174 | 			, 'methods'
175 | 			, $method_name
176 | 		);
177 | 
178 | 		$this->assertInternalType( 'array', $method_data );
179 | 		$this->assertEntityUses( $method_data, $type, $entity );
180 | 	}
181 | 
182 | 	/**
183 | 	 * Assert that a method doesn't use another entity.
184 | 	 *
185 | 	 * @param string $type        The type of entity. E.g. 'functions', 'methods'.
186 | 	 * @param string $class_name  The name of the class that the method is used in.
187 | 	 * @param string $method_name The name of the method that uses this method.
188 | 	 * @param array  $entity      The expected exported data for this entity.
189 | 	 */
190 | 	protected function assertMethodNotUses( $type, $class_name, $method_name, $entity ) {
191 | 
192 | 		$class_data = $this->find_entity_data_in(
193 | 			$this->export_data
194 | 			, 'classes'
195 | 			, $class_name
196 | 		);
197 | 
198 | 		$this->assertInternalType( 'array', $class_data );
199 | 
200 | 		$method_data = $this->find_entity_data_in(
201 | 			$class_data
202 | 			, 'methods'
203 | 			, $method_name
204 | 		);
205 | 
206 | 		$this->assertInternalType( 'array', $method_data );
207 | 		$this->assertEntityNotUses( $method_data, $type, $entity );
208 | 	}
209 | 
210 | 	/**
211 | 	 * Assert that a file uses a function.
212 | 	 *
213 | 	 * @param array $function The expected export data for the function.
214 | 	 */
215 | 	protected function assertFileUsesFunction( $function ) {
216 | 
217 | 		$this->assertEntityUses( $this->export_data, 'functions', $function );
218 | 	}
219 | 
220 | 	/**
221 | 	 * Assert that a function uses another function.
222 | 	 *
223 | 	 * @param string $function_name The name of the function that uses this function.
224 | 	 * @param array  $function      The expected exported data for the used function.
225 | 	 */
226 | 	protected function assertFunctionUsesFunction( $function_name, $function ) {
227 | 
228 | 		$this->assertFunctionUses( 'functions', $function_name, $function );
229 | 	}
230 | 
231 | 	/**
232 | 	 * Assert that a method uses a function.
233 | 	 *
234 | 	 * @param string $class_name  The name of the class that the method is used in.
235 | 	 * @param string $method_name The name of the method that uses this method.
236 | 	 * @param array  $function    The expected exported data for this function.
237 | 	 */
238 | 	protected function assertMethodUsesFunction( $class_name, $method_name, $function ) {
239 | 
240 | 		$this->assertMethodUses( 'functions', $class_name, $method_name, $function );
241 | 	}
242 | 
243 | 	/**
244 | 	 * Assert that a file uses a function.
245 | 	 *
246 | 	 * @param array $function The expected export data for the function.
247 | 	 */
248 | 	protected function assertFileNotUsesFunction( $function ) {
249 | 
250 | 		$this->assertEntityNotUses( $this->export_data, 'functions', $function );
251 | 	}
252 | 
253 | 	/**
254 | 	 * Assert that a function uses another function.
255 | 	 *
256 | 	 * @param string $function_name The name of the function that uses this function.
257 | 	 * @param array  $function      The expected exported data for the used function.
258 | 	 */
259 | 	protected function assertFunctionNotUsesFunction( $function_name, $function ) {
260 | 
261 | 		$this->assertFunctionNotUses( 'functions', $function_name, $function );
262 | 	}
263 | 
264 | 	/**
265 | 	 * Assert that a method uses a function.
266 | 	 *
267 | 	 * @param string $class_name  The name of the class that the method is used in.
268 | 	 * @param string $method_name The name of the method that uses this method.
269 | 	 * @param array  $function    The expected exported data for this function.
270 | 	 */
271 | 	protected function assertMethodNotUsesFunction( $class_name, $method_name, $function ) {
272 | 
273 | 		$this->assertMethodNotUses( 'functions', $class_name, $method_name, $function );
274 | 	}
275 | 
276 | 	/**
277 | 	 * Assert that a file uses an method.
278 | 	 *
279 | 	 * @param array $method The expected export data for the method.
280 | 	 */
281 | 	protected function assertFileUsesMethod( $method ) {
282 | 
283 | 		$this->assertEntityUses( $this->export_data, 'methods', $method );
284 | 	}
285 | 
286 | 	/**
287 | 	 * Assert that a function uses a method.
288 | 	 *
289 | 	 * @param string $function_name The name of the function that uses this method.
290 | 	 * @param array  $method        The expected exported data for this method.
291 | 	 */
292 | 	protected function assertFunctionUsesMethod( $function_name, $method ) {
293 | 
294 | 		$this->assertFunctionUses( 'methods', $function_name, $method );
295 | 	}
296 | 
297 | 	/**
298 | 	 * Assert that a method uses a method.
299 | 	 *
300 | 	 * @param string $class_name  The name of the class that the method is used in.
301 | 	 * @param string $method_name The name of the method that uses this method.
302 | 	 * @param array  $method      The expected exported data for this method.
303 | 	 */
304 | 	protected function assertMethodUsesMethod( $class_name, $method_name, $method ) {
305 | 
306 | 		$this->assertMethodUses( 'methods', $class_name, $method_name, $method );
307 | 	}
308 | 
309 | 	/**
310 | 	 * Assert that a file uses an method.
311 | 	 *
312 | 	 * @param array $method The expected export data for the method.
313 | 	 */
314 | 	protected function assertFileNotUsesMethod( $method ) {
315 | 
316 | 		$this->assertEntityNotUses( $this->export_data, 'methods', $method );
317 | 	}
318 | 
319 | 	/**
320 | 	 * Assert that a function uses a method.
321 | 	 *
322 | 	 * @param string $function_name The name of the function that uses this method.
323 | 	 * @param array  $method        The expected exported data for this method.
324 | 	 */
325 | 	protected function assertFunctionNotUsesMethod( $function_name, $method ) {
326 | 
327 | 		$this->assertFunctionNotUses( 'methods', $function_name, $method );
328 | 	}
329 | 
330 | 	/**
331 | 	 * Assert that a method uses a method.
332 | 	 *
333 | 	 * @param string $class_name  The name of the class that the method is used in.
334 | 	 * @param string $method_name The name of the method that uses this method.
335 | 	 * @param array  $method      The expected exported data for this method.
336 | 	 */
337 | 	protected function assertMethodNotUsesMethod( $class_name, $method_name, $method ) {
338 | 
339 | 		$this->assertMethodNotUses( 'methods', $class_name, $method_name, $method );
340 | 	}
341 | 
342 | 	/**
343 | 	 * Assert that an entity has a docblock.
344 | 	 *
345 | 	 * @param array  $entity  The exported entity data.
346 | 	 * @param array  $docs    The expected data for the entity's docblock.
347 | 	 * @param string $doc_key The key in the entity array that should hold the docs.
348 | 	 */
349 | 	protected function assertEntityHasDocs( $entity, $docs, $doc_key = 'doc' ) {
350 | 
351 | 		$this->assertArrayHasKey( $doc_key, $entity );
352 | 
353 | 		foreach ( $docs as $key => $expected_value ) {
354 | 			$this->assertEquals( $expected_value, $entity[ $doc_key ][ $key ] );
355 | 		}
356 | 	}
357 | 
358 | 	/**
359 | 	 * Assert that a file has a docblock.
360 | 	 *
361 | 	 * @param array $docs The expected data for the file's docblock.
362 | 	 */
363 | 	protected function assertFileHasDocs( $docs ) {
364 | 
365 | 		$this->assertEntityHasDocs( $this->export_data, $docs, 'file' );
366 | 	}
367 | 
368 | 	/**
369 | 	 * Assert that a function has a docblock.
370 | 	 *
371 | 	 * @param array $func The function name.
372 | 	 * @param array $docs The expected data for the function's docblock.
373 | 	 */
374 | 	protected function assertFunctionHasDocs( $func, $docs ) {
375 | 
376 | 		$func = $this->find_entity_data_in( $this->export_data, 'functions', $func );
377 | 		$this->assertEntityHasDocs( $func, $docs );
378 | 	}
379 | 
380 | 	/**
381 | 	 * Assert that a class has a docblock.
382 | 	 *
383 | 	 * @param array $class The class name.
384 | 	 * @param array $docs  The expected data for the class's docblock.
385 | 	 */
386 | 	protected function assertClassHasDocs( $class, $docs ) {
387 | 
388 | 		$class = $this->find_entity_data_in( $this->export_data, 'classes', $class );
389 | 		$this->assertEntityHasDocs( $class, $docs );
390 | 	}
391 | 
392 | 	/**
393 | 	 * Assert that a method has a docblock.
394 | 	 *
395 | 	 * @param string $class  The name of the class that the method is used in.
396 | 	 * @param string $method The method name.
397 | 	 * @param array  $docs   The expected data for the method's docblock.
398 | 	 */
399 | 	protected function assertMethodHasDocs( $class, $method, $docs ) {
400 | 
401 | 		$class = $this->find_entity_data_in( $this->export_data, 'classes', $class );
402 | 		$this->assertInternalType( 'array', $class );
403 | 
404 | 		$method = $this->find_entity_data_in( $class, 'methods', $method );
405 | 		$this->assertEntityHasDocs( $method, $docs );
406 | 	}
407 | 
408 | 	/**
409 | 	 * Assert that a property has a docblock.
410 | 	 *
411 | 	 * @param string $class    The name of the class that the method is used in.
412 | 	 * @param string $property The property name.
413 | 	 * @param array  $docs     The expected data for the property's docblock.
414 | 	 */
415 | 	protected function assertPropertyHasDocs( $class, $property, $docs ) {
416 | 
417 | 		$class = $this->find_entity_data_in( $this->export_data, 'classes', $class );
418 | 		$this->assertInternalType( 'array', $class );
419 | 
420 | 		$property = $this->find_entity_data_in( $class, 'properties', $property );
421 | 		$this->assertEntityHasDocs( $property, $docs );
422 | 	}
423 | 
424 | 	/**
425 | 	 * Assert that a hook has a docblock.
426 | 	 *
427 | 	 * @param array $hook The hook name.
428 | 	 * @param array $docs The expected data for the hook's docblock.
429 | 	 */
430 | 	protected function assertHookHasDocs( $hook, $docs ) {
431 | 
432 | 		$hook = $this->find_entity_data_in( $this->export_data, 'hooks', $hook );
433 | 		$this->assertEntityHasDocs( $hook, $docs );
434 | 	}
435 | 
436 | 	/**
437 | 	 * Find the exported data for an entity.
438 | 	 *
439 | 	 * @param array  $data        The data to search in.
440 | 	 * @param string $type        The type of entity.
441 | 	 * @param string $entity_name The name of the function.
442 | 	 *
443 | 	 * @return array|false The data for the entity, or false if it couldn't be found.
444 | 	 */
445 | 	protected function find_entity_data_in( $data, $type, $entity ) {
446 | 
447 | 		if ( empty( $data[ $type ] ) ) {
448 | 			return false;
449 | 		}
450 | 
451 | 		foreach ( $data[ $type ] as $entity_data ) {
452 | 			if ( $entity_data['name'] === $entity ) {
453 | 				return $entity_data;
454 | 			}
455 | 		}
456 | 
457 | 		return false;
458 | 	}
459 | 
460 | 	/**
461 | 	 * Check if one entity uses another entity.
462 | 	 *
463 | 	 * @param array  $entity The exported entity data.
464 | 	 * @param string $type   The type of thing that this entity should use.
465 | 	 * @param array  $used   The expected data for the thing the entity should use.
466 | 	 *
467 | 	 * @return bool Whether the entity uses the other.
468 | 	 */
469 | 	function entity_uses( $entity, $type, $used ) {
470 | 
471 | 		if ( ! isset( $entity['uses'][ $type ] ) ) {
472 | 			return false;
473 | 		}
474 | 
475 | 		foreach ( $entity['uses'][ $type ] as $exported_used ) {
476 | 			if ( $exported_used['line'] == $used['line'] ) {
477 | 				$this->assertEquals( $used, $exported_used );
478 | 				return true;
479 | 			}
480 | 		}
481 | 
482 | 		return false;
483 | 	}
484 | }
485 | 


--------------------------------------------------------------------------------
/tests/phpunit/tests/export/docblocks.inc:
--------------------------------------------------------------------------------
 1 | assertStringMatchesFormat(
 20 | 			'%s'
 21 | 			, $this->export_data['classes'][0]['doc']['long_description']
 22 | 		);
 23 | 	}
 24 | 
 25 | 	/**
 26 | 	 * Test that hooks which aren't documented don't receive docs from another node.
 27 | 	 */
 28 | 	public function test_undocumented_hook() {
 29 | 
 30 | 		$this->assertHookHasDocs(
 31 | 			'undocumented_hook'
 32 | 			, array(
 33 | 				'description' => '',
 34 | 			)
 35 | 		);
 36 | 	}
 37 | 
 38 | 	/**
 39 | 	 * Test that hook docbloks are picked up.
 40 | 	 */
 41 | 	public function test_hook_docblocks() {
 42 | 
 43 | 		$this->assertHookHasDocs(
 44 | 			'test_action'
 45 | 			, array( 'description' => 'A test action.' )
 46 | 		);
 47 | 
 48 | 		$this->assertHookHasDocs(
 49 | 			'test_filter'
 50 | 			, array( 'description' => 'A filter.' )
 51 | 		);
 52 | 
 53 | 		$this->assertHookHasDocs(
 54 | 			'test_ref_array_action'
 55 | 			, array( 'description' => 'A reference array action.' )
 56 | 		);
 57 | 
 58 | 		$this->assertHookHasDocs(
 59 | 			'test_ref_array_filter'
 60 | 			, array( 'description' => 'A reference array filter.' )
 61 | 		);
 62 | 	}
 63 | 
 64 | 	/**
 65 | 	 * Test that file-level docs are exported.
 66 | 	 */
 67 | 	public function test_file_docblocks() {
 68 | 
 69 | 		$this->assertFileHasDocs(
 70 | 			array( 'description' => 'This is the file-level docblock summary.' )
 71 | 		);
 72 | 	}
 73 | 
 74 | 	/**
 75 | 	 * Test that function docs are exported.
 76 | 	 */
 77 | 	public function test_function_docblocks() {
 78 | 
 79 | 		$this->assertFunctionHasDocs(
 80 | 			'test_func'
 81 | 			, array(
 82 | 				'description' => 'This is a function docblock.',
 83 | 				'long_description' => '

This function is just a test, but we\'ve added this description anyway.

', 84 | 'tags' => array( 85 | array( 86 | 'name' => 'since', 87 | 'content' => '2.6.0', 88 | ), 89 | array( 90 | 'name' => 'param', 91 | 'content' => 'A string value.', 92 | 'types' => array( 'string' ), 93 | 'variable' => '$var', 94 | ), 95 | array( 96 | 'name' => 'param', 97 | 'content' => 'A number.', 98 | 'types' => array( 'int' ), 99 | 'variable' => '$num', 100 | ), 101 | array( 102 | 'name' => 'return', 103 | 'content' => 'Whether the function was called correctly.', 104 | 'types' => array( 'bool' ), 105 | ), 106 | ), 107 | ) 108 | ); 109 | } 110 | 111 | /** 112 | * Test that class docs are exported. 113 | */ 114 | public function test_class_docblocks() { 115 | 116 | $this->assertClassHasDocs( 117 | 'Test_Class' 118 | , array( 'description' => 'This is a class docblock.' ) 119 | ); 120 | } 121 | 122 | /** 123 | * Test that method docs are exported. 124 | */ 125 | public function test_method_docblocks() { 126 | 127 | $this->assertMethodHasDocs( 128 | 'Test_Class' 129 | , 'test_method' 130 | , array( 'description' => 'This is a method docblock.' ) 131 | ); 132 | } 133 | 134 | /** 135 | * Test that function docs are exported. 136 | */ 137 | public function test_property_docblocks() { 138 | 139 | $this->assertPropertyHasDocs( 140 | 'Test_Class' 141 | , '$a_string' 142 | , array( 'description' => 'This is a docblock for a class property.' ) 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/hooks.inc: -------------------------------------------------------------------------------- 1 | property . '_pre' ); 8 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/hooks.php: -------------------------------------------------------------------------------- 1 | assertFileContainsHook( 20 | array( 'name' => 'plain_action', 'line' => 3 ) 21 | ); 22 | 23 | $this->assertFileContainsHook( 24 | array( 'name' => 'action_with_double_quotes', 'line' => 4 ) 25 | ); 26 | 27 | $this->assertFileContainsHook( 28 | array( 'name' => '{$variable}-action', 'line' => 5 ) 29 | ); 30 | 31 | $this->assertFileContainsHook( 32 | array( 'name' => 'another-{$variable}-action', 'line' => 6 ) 33 | ); 34 | 35 | $this->assertFileContainsHook( 36 | array( 'name' => 'hook_{$object->property}_pre', 'line' => 7 ) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/namespace.inc: -------------------------------------------------------------------------------- 1 | export_data['functions'][0]['namespace']; 20 | 21 | $this->assertEquals( $expected, $actual, 'Namespace should be parsed' ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/uses/constructor.inc: -------------------------------------------------------------------------------- 1 | assertFileUsesMethod( 20 | array( 21 | 'name' => '__construct', 22 | 'line' => 3, 23 | 'end_line' => 3, 24 | 'class' => '\WP_Query', 25 | 'static' => false, 26 | ) 27 | ); 28 | 29 | $this->assertFunctionUsesMethod( 30 | 'test' 31 | , array( 32 | 'name' => '__construct', 33 | 'line' => 6, 34 | 'end_line' => 6, 35 | 'class' => '\My_Class', 36 | 'static' => false, 37 | ) 38 | ); 39 | } 40 | 41 | /** 42 | * Test that use is exported when the self keyword is used. 43 | */ 44 | public function test_new_self() { 45 | 46 | $this->assertMethodUsesMethod( 47 | 'My_Class' 48 | , 'instance' 49 | , array( 50 | 'name' => '__construct', 51 | 'line' => 12, 52 | 'end_line' => 12, 53 | 'class' => '\My_Class', 54 | 'static' => false, 55 | ) 56 | ); 57 | } 58 | 59 | /** 60 | * Test that use is exported when the parent keyword is used. 61 | */ 62 | public function test_new_parent() { 63 | 64 | $this->assertMethodUsesMethod( 65 | 'My_Class' 66 | , 'parent' 67 | , array( 68 | 'name' => '__construct', 69 | 'line' => 16, 70 | 'end_line' => 16, 71 | 'class' => '\Parent_Class', 72 | 'static' => false, 73 | ) 74 | ); 75 | } 76 | 77 | /** 78 | * Test that use is exported when a variable is used. 79 | */ 80 | public function test_new_variable() { 81 | 82 | $this->assertFileUsesMethod( 83 | array( 84 | 'name' => '__construct', 85 | 'line' => 20, 86 | 'end_line' => 20, 87 | 'class' => '$class', 88 | 'static' => false, 89 | ) 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/uses/methods.inc: -------------------------------------------------------------------------------- 1 | update( $table, $data, $where ); 6 | 7 | function test() { 8 | Another_Class::another_method(); 9 | 10 | get_class()->call_method(); 11 | } 12 | 13 | class My_Class extends Parent_Class { 14 | 15 | static function static_method() { 16 | Another_Class::do_static_stuff(); 17 | self::do_stuff(); 18 | $this->go(); 19 | parent::do_parental_stuff(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/uses/methods.php: -------------------------------------------------------------------------------- 1 | assertFileUsesMethod( 20 | array( 21 | 'name' => 'static_method', 22 | 'line' => 3, 23 | 'end_line' => 3, 24 | 'class' => '\My_Class', 25 | 'static' => true, 26 | ) 27 | ); 28 | 29 | $this->assertFunctionUsesMethod( 30 | 'test' 31 | , array( 32 | 'name' => 'another_method', 33 | 'line' => 8, 34 | 'end_line' => 8, 35 | 'class' => '\Another_Class', 36 | 'static' => true, 37 | ) 38 | ); 39 | 40 | $this->assertMethodUsesMethod( 41 | 'My_Class' 42 | , 'static_method' 43 | , array( 44 | 'name' => 'do_static_stuff', 45 | 'line' => 16, 46 | 'end_line' => 16, 47 | 'class' => '\Another_Class', 48 | 'static' => true, 49 | ) 50 | ); 51 | 52 | $this->assertMethodUsesMethod( 53 | 'My_Class' 54 | , 'static_method' 55 | , array( 56 | 'name' => 'do_stuff', 57 | 'line' => 17, 58 | 'end_line' => 17, 59 | 'class' => '\My_Class', 60 | 'static' => true, 61 | ) 62 | ); 63 | 64 | $this->assertMethodUsesMethod( 65 | 'My_Class' 66 | , 'static_method' 67 | , array( 68 | 'name' => 'do_parental_stuff', 69 | 'line' => 19, 70 | 'end_line' => 19, 71 | 'class' => '\Parent_Class', 72 | 'static' => true, 73 | ) 74 | ); 75 | } 76 | 77 | /** 78 | * Test that instance method use is exported. 79 | */ 80 | public function test_instance_methods() { 81 | 82 | $this->assertFileUsesMethod( 83 | array( 84 | 'name' => 'update', 85 | 'line' => 5, 86 | 'end_line' => 5, 87 | 'class' => '$wpdb', 88 | 'static' => false, 89 | ) 90 | ); 91 | 92 | $this->assertFunctionUsesMethod( 93 | 'test' 94 | , array( 95 | 'name' => 'call_method', 96 | 'line' => 10, 97 | 'end_line' => 10, 98 | 'class' => 'get_class()', 99 | 'static' => false, 100 | ) 101 | ); 102 | 103 | $this->assertMethodUsesMethod( 104 | 'My_Class' 105 | , 'static_method' 106 | , array( 107 | 'name' => 'go', 108 | 'line' => 18, 109 | 'end_line' => 18, 110 | 'class' => '\My_Class', 111 | 'static' => false, 112 | ) 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/uses/nested.inc: -------------------------------------------------------------------------------- 1 | do_it(); 24 | 25 | function sub_method_test() { 26 | 27 | b_function(); 28 | 29 | My_Class::a_method(); 30 | } 31 | 32 | do_things(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/phpunit/tests/export/uses/nested.php: -------------------------------------------------------------------------------- 1 | assertFunctionUsesFunction( 20 | 'test' 21 | , array( 22 | 'name' => 'a_function', 23 | 'line' => 5, 24 | 'end_line' => 5, 25 | ) 26 | ); 27 | 28 | $this->assertFunctionUsesFunction( 29 | 'test' 30 | , array( 31 | 'name' => 'sub_test', 32 | 'line' => 14, 33 | 'end_line' => 14, 34 | ) 35 | ); 36 | 37 | $this->assertFunctionUsesMethod( 38 | 'test' 39 | , array( 40 | 'name' => 'do_things', 41 | 'line' => 16, 42 | 'end_line' => 16, 43 | 'class' => '\My_Class', 44 | 'static' => true, 45 | ) 46 | ); 47 | 48 | $this->assertFunctionNotUsesFunction( 49 | 'test' 50 | , array( 51 | 'name' => 'b_function', 52 | 'line' => 9, 53 | 'end_line' => 9, 54 | ) 55 | ); 56 | 57 | $this->assertFunctionNotUsesMethod( 58 | 'test' 59 | , array( 60 | 'name' => 'static_method', 61 | 'line' => 11, 62 | 'end_line' => 11, 63 | 'class' => '\My_Class', 64 | 'static' => true, 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * Test that the usages of the nested function is correct. 71 | */ 72 | public function test_nested_function_uses_correct() { 73 | 74 | $this->assertFunctionUsesFunction( 75 | 'sub_test' 76 | , array( 77 | 'name' => 'b_function', 78 | 'line' => 9, 79 | 'end_line' => 9, 80 | ) 81 | ); 82 | 83 | $this->assertFunctionUsesMethod( 84 | 'sub_test' 85 | , array( 86 | 'name' => 'static_method', 87 | 'line' => 11, 88 | 'end_line' => 11, 89 | 'class' => '\My_Class', 90 | 'static' => true, 91 | ) 92 | ); 93 | 94 | $this->assertFunctionNotUsesFunction( 95 | 'sub_test' 96 | , array( 97 | 'name' => 'a_function', 98 | 'line' => 5, 99 | 'end_line' => 5, 100 | ) 101 | ); 102 | 103 | $this->assertFunctionNotUsesFunction( 104 | 'sub_test' 105 | , array( 106 | 'name' => 'sub_test', 107 | 'line' => 14, 108 | 'end_line' => 14, 109 | ) 110 | ); 111 | 112 | $this->assertFunctionNotUsesMethod( 113 | 'sub_test' 114 | , array( 115 | 'name' => 'do_things', 116 | 'line' => 16, 117 | 'end_line' => 16, 118 | ) 119 | ); 120 | } 121 | 122 | 123 | /** 124 | * Test that the uses data of the outer method is correct. 125 | */ 126 | public function test_method_uses_correct() { 127 | 128 | $this->assertMethodUsesMethod( 129 | 'My_Class' 130 | , 'a_method' 131 | , array( 132 | 'name' => 'do_it', 133 | 'line' => 23, 134 | 'end_line' => 23, 135 | 'class' => '\My_Class', 136 | 'static' => false, 137 | ) 138 | ); 139 | 140 | $this->assertMethodUsesFunction( 141 | 'My_Class' 142 | , 'a_method' 143 | , array( 144 | 'name' => 'do_things', 145 | 'line' => 32, 146 | 'end_line' => 32, 147 | ) 148 | ); 149 | 150 | $this->assertMethodNotUsesFunction( 151 | 'My_Class' 152 | , 'a_method' 153 | , array( 154 | 'name' => 'b_function', 155 | 'line' => 27, 156 | 'end_line' => 27, 157 | ) 158 | ); 159 | 160 | $this->assertMethodNotUsesMethod( 161 | 'My_Class' 162 | , 'a_method' 163 | , array( 164 | 'name' => 'a_method', 165 | 'line' => 29, 166 | 'end_line' => 29, 167 | 'class' => '\My_Class', 168 | 'static' => true, 169 | ) 170 | ); 171 | } 172 | 173 | /** 174 | * Test that the usages of the nested function within a method is correct. 175 | */ 176 | public function test_nested_function_in_method_uses_correct() { 177 | 178 | $this->assertFunctionUsesFunction( 179 | 'sub_method_test' 180 | , array( 181 | 'name' => 'b_function', 182 | 'line' => 27, 183 | 'end_line' => 27, 184 | ) 185 | ); 186 | 187 | $this->assertFunctionUsesMethod( 188 | 'sub_method_test' 189 | , array( 190 | 'name' => 'a_method', 191 | 'line' => 29, 192 | 'end_line' => 29, 193 | 'class' => '\My_Class', 194 | 'static' => true, 195 | ) 196 | ); 197 | 198 | $this->assertFunctionNotUsesMethod( 199 | 'sub_method_test' 200 | , array( 201 | 'name' => 'do_it', 202 | 'line' => 23, 203 | 'end_line' => 23, 204 | 'class' => '\My_Class', 205 | 'static' => false, 206 | ) 207 | ); 208 | 209 | $this->assertFunctionNotUsesFunction( 210 | 'sub_method_test' 211 | , array( 212 | 'name' => 'do_things', 213 | 'line' => 32, 214 | 'end_line' => 32, 215 | ) 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/source/actions.php: -------------------------------------------------------------------------------- 1 | relate_method4(); 25 | } 26 | 27 | function relate_function5() { 28 | wpdb::relate_method2()->some_function(); 29 | } 30 | 31 | function relate_function6() { 32 | wp_screen()->relate_method1(); 33 | } 34 | 35 | class wpdb { 36 | 37 | public function __construct() {} 38 | 39 | public static function relate_method1() { 40 | self::relate_method2(); 41 | } 42 | 43 | public static function relate_method2() { 44 | /** 45 | * Filter a aCustomize setting value in un-slashed form. 46 | * 47 | * @since 3.5.0 48 | * 49 | * @param mixed $value Value of the setting. 50 | * @param WP_Customize_Setting $this WP_Customize_Setting instance. 51 | */ 52 | $meh = apply_filters( 'meh-hook', $meh ); 53 | } 54 | 55 | public static function relate_method3() { 56 | relate_function1(); 57 | } 58 | 59 | public function relate_method4() { 60 | relate_function2(); 61 | } 62 | 63 | public function relate_method5() { 64 | $this->relate_method4(); 65 | } 66 | 67 | public static function relate_method6() { 68 | wpdb::relate_method1(); 69 | } 70 | 71 | public static function relate_method7() { 72 | global $wpdb; 73 | 74 | $wpdb->relate_method5(); 75 | } 76 | } 77 | 78 | class WP_Screen { 79 | public function relate_method1() {} 80 | } 81 | --------------------------------------------------------------------------------