Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
MWDoxygenFilter
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
 filter
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Copyright (C) 2012 Tamas Imrei <tamas.imrei@gmail.com> https://virtualtee.blogspot.com/
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Maintenance
22 */
23
24/**
25 * Doxygen filter to show correct member variable types in documentation.
26 *
27 * Based on
28 * <https://virtualtee.blogspot.co.uk/2012/03/tip-for-using-doxygen-for-php-code.html>
29 *
30 * It has been adapted for MediaWiki to resolve various bugs we experienced
31 * from using Doxygen with our coding conventions:
32 *
33 * - We want to allow documenting class members on a single line by documenting
34 *   them as `/** @var SomeType Description here.`, and in long-form as
35 *   `/**\n * Description here.\n * @var SomeType`.
36 *
37 * - PHP does not support native type-hinting of class members. Instead, we document
38 *   that using `@var` in the doc blocks above it. However, Doxygen only supports
39 *   parsing this from executable code. We achieve this by having the below filter
40 *   take the typehint from the doc block and insert it into the source code in
41 *   front of `$myvar`, like `protected SomeType $myvar`. This result is technically
42 *   invalid PHP code, but Doxygen understands it this way.
43 *
44 * @internal For use by maintenance/mwdoc-filter.php
45 * @ingroup Maintenance
46 */
47class MWDoxygenFilter {
48    /**
49     * @param string $source Original source code
50     * @return string Filtered source code
51     */
52    public static function filter( $source ) {
53        $tokens = token_get_all( $source );
54        $buffer = null;
55        $output = '';
56        foreach ( $tokens as $token ) {
57            if ( is_string( $token ) ) {
58                if ( $buffer !== null && $token === ';' ) {
59                    // If we still have a buffer and the statement has ended,
60                    // flush it and move on.
61                    $output .= $buffer['raw'];
62                    $buffer = null;
63                }
64                $output .= $token;
65                continue;
66            }
67            [ $id, $content ] = $token;
68            switch ( $id ) {
69                case T_DOC_COMMENT:
70                    // Escape slashes so that references to namespaces are not
71                    // wrongly interpreted as a Doxygen "\command".
72                    $content = addcslashes( $content, '\\' );
73                    // Look for instances of "@var SomeType".
74                    if ( preg_match( '#@var\s+\S+#', $content ) ) {
75                        $buffer = [ 'raw' => $content, 'desc' => null, 'type' => null, 'name' => null ];
76                        $buffer['desc'] = preg_replace_callback(
77                            // Strip "@var SomeType" part, but remember the type and optional name
78                            '#@var\s+(\S+)(\s+)?(\S+)?#',
79                            static function ( $matches ) use ( &$buffer ) {
80                                $buffer['type'] = $matches[1];
81                                $buffer['name'] = $matches[3] ?? null;
82                                return ( $matches[2] ?? '' ) . ( $matches[3] ?? '' );
83                            },
84                            $content
85                        );
86                    } else {
87                        $output .= $content;
88                    }
89                    break;
90
91                case T_VARIABLE:
92                    // Doxygen requires class members to be documented in one of two ways:
93                    //
94                    // 1. Fully qualified:
95                    //    /** @var SomeType $name Description here. */
96                    //
97                    //    These result in the creation of a new virtual node called $name
98                    //    with the specified type and description. The real code doesn't
99                    //    even need to exist in this case.
100                    //
101                    // 2. Contextual:
102                    //    /** Description here. */
103                    //    private SomeType? $name;
104                    //
105                    // In MediaWiki, we are mostly like #1 but without the name repeated:
106                    //    /** @var SomeType Description here. */
107                    //    private $name;
108                    //
109                    // These emit a warning in Doxygen because they are missing a variable name.
110                    // Convert these to the "Contextual" kind by stripping ""@var", injecting
111                    // type into the code, and leaving the description in-place.
112                    if ( $buffer !== null ) {
113                        if ( $buffer['name'] === $content ) {
114                            // Fully qualitied "@var" comment, leave as-is.
115                            $output .= $buffer['raw'];
116                            $output .= $content;
117                        } else {
118                            // MW-style "@var" comment. Keep only the description and transplant
119                            // the type into the code.
120                            $output .= $buffer['desc'];
121                            $output .= "{$buffer['type']} $content";
122                        }
123                        $buffer = null;
124                    } else {
125                        $output .= $content;
126                    }
127                    break;
128
129                default:
130                    if ( $buffer !== null ) {
131                        $buffer['raw'] .= $content;
132                        $buffer['desc'] .= $content;
133                    } else {
134                        $output .= $content;
135                    }
136                    break;
137            }
138        }
139        return $output;
140    }
141}