| 1 | <?php |
| 2 | |
| 3 | require_once dirname( dirname( dirname( __FILE__ ) ) ) . '/vendor/autoload.php'; |
| 4 | |
| 5 | class Tests_DocBlocks extends WP_UnitTestCase { |
| 6 | |
| 7 | protected static $ignore_empty = true; |
| 8 | protected static $method_refs = array(); |
| 9 | protected static $function_refs = array(); |
| 10 | |
| 11 | public function remove_variadic_params( $param_doc ) { |
| 12 | return ( false === strpos( $param_doc, ',...' ) ); |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * Test a function or method for a given class |
| 17 | * |
| 18 | * @dataProvider dataReflectionTestFunctions |
| 19 | * |
| 20 | * @param string|array $function The function name, or array of class name and method name. |
| 21 | * @param string $file The file name. |
| 22 | */ |
| 23 | public function testFunctionDocBlock( $function, $filename ) { |
| 24 | |
| 25 | if ( is_array( $function ) ) { |
| 26 | $name = $function[0] . '::' . $function[1] . '()'; |
| 27 | $ref = self::$method_refs[ $filename ][ $function[0] ][ $name ]; |
| 28 | } else { |
| 29 | $name = $function; |
| 30 | $ref = self::$function_refs[ $filename ][ $name ]; |
| 31 | } |
| 32 | |
| 33 | $name = ltrim( $name, '\\' ); |
| 34 | $docblock = $ref->getDocblock(); |
| 35 | |
| 36 | if ( self::$ignore_empty && empty( $docblock ) ) { |
| 37 | return; |
| 38 | } |
| 39 | |
| 40 | $this->assertNotEmpty( $docblock, sprintf( |
| 41 | 'The docblock for `%s` should not be missing.', |
| 42 | $name |
| 43 | ) ); |
| 44 | |
| 45 | $method_params = $ref->getArguments(); |
| 46 | $doc_params = $docblock->getTagsByName( 'param' ); |
| 47 | $desc = $docblock->getDescription(); |
| 48 | |
| 49 | if ( self::$ignore_empty && empty( $desc ) ) { |
| 50 | return; |
| 51 | } |
| 52 | |
| 53 | $this->assertNotEmpty( $desc, sprintf( |
| 54 | 'The docblock description for `%s` should not be empty.', |
| 55 | $name |
| 56 | ) ); |
| 57 | |
| 58 | if ( count( $method_params ) < count( $doc_params ) ) { |
| 59 | $non_variadic_doc_params = array_filter( $doc_params, array( $this, 'remove_variadic_params' ) ); |
| 60 | } else { |
| 61 | $non_variadic_doc_params = $doc_params; |
| 62 | } |
| 63 | |
| 64 | $this->assertSame( count( $method_params ), count( $non_variadic_doc_params ), sprintf( |
| 65 | 'The number of @param docs for `%s` should match its number of parameters.', |
| 66 | $name |
| 67 | ) ); |
| 68 | |
| 69 | foreach ( $method_params as $i => $param ) { |
| 70 | |
| 71 | $param_doc = $doc_params[ $i ]; |
| 72 | $description = (string) $param_doc->getDescription(); |
| 73 | $is_hash = ( ( 0 === strpos( $description, '{' ) ) && ( ( strlen( $description ) - 1 ) === strrpos( $description, '}' ) ) ); |
| 74 | |
| 75 | if ( $is_hash ) { |
| 76 | $lines = explode( "\n", $description ); |
| 77 | $description = $lines[1]; |
| 78 | } |
| 79 | |
| 80 | if ( self::$ignore_empty && empty( $description ) ) { |
| 81 | return; |
| 82 | } |
| 83 | |
| 84 | $this->assertNotEmpty( $description, sprintf( |
| 85 | 'The @param description for the `$%s` parameter of `%s` should not be empty.', |
| 86 | $param_doc->getVariableName(), |
| 87 | $name |
| 88 | ) ); |
| 89 | |
| 90 | $param_doc_name = $param_doc->getVariableName(); |
| 91 | $param_doc_type = (string) $param_doc->getType(); |
| 92 | |
| 93 | $this->assertSame( $param->getName(), $param_doc_name, sprintf( |
| 94 | 'The @param name for the `%s` parameter of `%s` is incorrect.', |
| 95 | $param->getName(), |
| 96 | $name |
| 97 | ) ); |
| 98 | |
| 99 | // if ( $param->isOptional() ) { |
| 100 | // $this->assertNotFalse( strpos( $description, 'Optional' ), sprintf( |
| 101 | // 'The @param description for the optional `%s` parameter of `%s` should state that it is optional.', |
| 102 | // $param_doc_name, |
| 103 | // $name |
| 104 | // ) ); |
| 105 | // } else { |
| 106 | // $this->assertFalse( strpos( $description, 'Optional.' ), sprintf( |
| 107 | // 'The @param description for the required `%s` parameter of `%s` should not state that it is optional.', |
| 108 | // $param_doc_name, |
| 109 | // $name |
| 110 | // ) ); |
| 111 | // } |
| 112 | |
| 113 | // if ( $param->isDefaultValueAvailable() ) { |
| 114 | // $this->assertNotFalse( strpos( $description, 'Default ' ), sprintf( |
| 115 | // 'The @param description for the `%s` parameter of `%s` should state its default value.', |
| 116 | // $param_doc_name, |
| 117 | // $name |
| 118 | // ) ); |
| 119 | // } else { |
| 120 | // $this->assertFalse( strpos( $description, 'Default ' ), sprintf( |
| 121 | // 'The @param description for the `%s` parameter of `%s` should not state a default value.', |
| 122 | // $param_doc_name, |
| 123 | // $name |
| 124 | // ) ); |
| 125 | // } |
| 126 | |
| 127 | } |
| 128 | |
| 129 | } |
| 130 | |
| 131 | protected static function rsearch( $folder, $ext ) { |
| 132 | $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $folder ) ); |
| 133 | |
| 134 | $files = array(); |
| 135 | |
| 136 | foreach ( $iterator as $file ) { |
| 137 | |
| 138 | if ( $file->isDir() ) { |
| 139 | continue; |
| 140 | } |
| 141 | if ( $ext !== $file->getExtension() ) { |
| 142 | continue; |
| 143 | } |
| 144 | |
| 145 | $files[] = $file->getPathname(); |
| 146 | |
| 147 | } |
| 148 | return $files; |
| 149 | } |
| 150 | |
| 151 | public function dataReflectionTestFunctions() { |
| 152 | |
| 153 | $to_search = ABSPATH . 'wp-includes/'; |
| 154 | |
| 155 | // Recursive: |
| 156 | $files = self::rsearch( $to_search, 'php' ); |
| 157 | |
| 158 | // Non-recursive |
| 159 | // $files = glob( $to_search . '*.php' ); |
| 160 | |
| 161 | $ignore_files = array( |
| 162 | |
| 163 | ABSPATH . 'wp-admin/includes/class-pclzip.php', |
| 164 | ABSPATH . 'wp-admin/includes/class-ftp.php', |
| 165 | ABSPATH . 'wp-admin/includes/class-ftp-pure.php', |
| 166 | ABSPATH . 'wp-admin/includes/class-ftp-sockets.php', |
| 167 | ABSPATH . 'wp-admin/includes/export.php', |
| 168 | ABSPATH . 'wp-admin/includes/file.php', |
| 169 | |
| 170 | ABSPATH . 'wp-includes/class-IXR.php', |
| 171 | ABSPATH . 'wp-includes/rss.php', |
| 172 | |
| 173 | ); |
| 174 | $files = array_diff( $files, $ignore_files ); |
| 175 | $files = array_filter( $files, function( $file ) { |
| 176 | |
| 177 | $ignore_dirs = array( |
| 178 | ABSPATH . 'wp-includes/ID3/', |
| 179 | ABSPATH . 'wp-includes/SimplePie/', |
| 180 | ABSPATH . 'wp-includes/Text/', |
| 181 | ); |
| 182 | |
| 183 | foreach ( $ignore_dirs as $dir ) { |
| 184 | if ( 0 === strpos( $file, $dir ) ) { |
| 185 | return false; |
| 186 | } |
| 187 | } |
| 188 | return true; |
| 189 | |
| 190 | } ); |
| 191 | $data = array(); |
| 192 | |
| 193 | $projectFactory = \phpDocumentor\Reflection\Php\ProjectFactory::createInstance(); |
| 194 | |
| 195 | foreach ( $files as $project_file ) { |
| 196 | try { |
| 197 | $project = $projectFactory->create( 'WordPress', array( $project_file ) ); |
| 198 | } catch ( InvalidArgumentException $e ) { |
| 199 | // This catches files where functions are defined within functions. |
| 200 | // eg. redirect_canonical()::lowercase_octets() in wp-includes/canonical.php |
| 201 | continue; |
| 202 | } |
| 203 | |
| 204 | foreach ( $project->getFiles() as $filename => $file ) { |
| 205 | |
| 206 | foreach ( $file->getFunctions() as $f => $func_ref ) { |
| 207 | |
| 208 | self::$function_refs[ $filename ][ $f ] = $func_ref; |
| 209 | |
| 210 | $data[] = array( |
| 211 | (string) $func_ref->getFqsen(), |
| 212 | $filename, |
| 213 | ); |
| 214 | |
| 215 | } |
| 216 | |
| 217 | foreach ( $file->getClasses() as $c => $class_ref ) { |
| 218 | |
| 219 | foreach ( $class_ref->getMethods() as $m => $method_ref ) { |
| 220 | |
| 221 | self::$method_refs[ $filename ][ $c ][ $m ] = $method_ref; |
| 222 | |
| 223 | $data[] = array( |
| 224 | array( |
| 225 | (string) $class_ref->getFqsen(), |
| 226 | $method_ref->getName(), |
| 227 | ), |
| 228 | $filename, |
| 229 | ); |
| 230 | |
| 231 | } |
| 232 | |
| 233 | } |
| 234 | |
| 235 | } |
| 236 | |
| 237 | } |
| 238 | |
| 239 | return $data; |
| 240 | |
| 241 | } |
| 242 | |
| 243 | } |