Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.16% covered (warning)
89.16%
74 / 83
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 3
FileAwareNodeVisitor
80.00% covered (warning)
80.00%
4 / 5
66.67% covered (warning)
66.67%
2 / 3
3.07
0.00% covered (danger)
0.00%
0 / 1
 enterNode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
DeprecatedInterfaceFinder
86.49% covered (warning)
86.49%
32 / 37
0.00% covered (danger)
0.00%
0 / 3
16.63
0.00% covered (danger)
0.00%
0 / 1
 getFoundNodes
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 isHardDeprecated
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
8.38
 enterNode
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
6.04
FindDeprecated
92.68% covered (success)
92.68%
38 / 41
50.00% covered (danger)
50.00%
2 / 4
12.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMwInstallPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFiles
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 execute
94.12% covered (success)
94.12%
32 / 34
0.00% covered (danger)
0.00%
0 / 1
9.02
1<?php
2/**
3 * Maintenance script that recursively scans MediaWiki's PHP source tree
4 * for deprecated functions and methods and pretty-prints the results.
5 *
6 * @license GPL-2.0-or-later
7 * @file
8 * @ingroup Maintenance
9 * @phan-file-suppress PhanUndeclaredProperty Lots of custom properties
10 */
11
12use MediaWiki\Maintenance\Maintenance;
13
14// @codeCoverageIgnoreStart
15require_once __DIR__ . '/Maintenance.php';
16require_once __DIR__ . '/../vendor/autoload.php';
17// @codeCoverageIgnoreEnd
18
19/**
20 * A PHPParser node visitor that associates each node with its file name.
21 */
22class FileAwareNodeVisitor extends PhpParser\NodeVisitorAbstract {
23    /** @var string|null */
24    private $currentFile = null;
25
26    /** @inheritDoc */
27    public function enterNode( PhpParser\Node $node ) {
28        $retVal = parent::enterNode( $node );
29        // TODO: Make this work without dynamic property (T423054).
30        // "Warning: Creation of dynamic property PhpParser\Node\Stmt\Namespace_::$filename is deprecated"
31        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
32        @( $node->filename = $this->currentFile );
33        return $retVal;
34    }
35
36    public function setCurrentFile( ?string $filename ) {
37        $this->currentFile = $filename;
38    }
39
40    public function getCurrentFile(): ?string {
41        return $this->currentFile;
42    }
43}
44
45/**
46 * A PHPParser node visitor that finds deprecated functions and methods.
47 */
48class DeprecatedInterfaceFinder extends FileAwareNodeVisitor {
49
50    /** @var string */
51    private $currentClass = null;
52
53    /** @var array[] */
54    private $foundNodes = [];
55
56    public function getFoundNodes(): array {
57        // Sort results by version, then by filename, then by name.
58        foreach ( $this->foundNodes as &$nodes ) {
59            uasort( $nodes, static function ( $a, $b ) {
60                return ( $a['filename'] . $a['name'] ) <=> ( $b['filename'] . $b['name'] );
61            } );
62        }
63        ksort( $this->foundNodes );
64        return $this->foundNodes;
65    }
66
67    /**
68     * Check whether a function or method includes a call to wfDeprecated(),
69     * indicating that it is a hard-deprecated interface.
70     * @param PhpParser\Node $node
71     * @return bool
72     */
73    public function isHardDeprecated( PhpParser\Node $node ) {
74        if ( !$node->stmts ) {
75            return false;
76        }
77        foreach ( $node->stmts as $stmt ) {
78            $functionExpression = null;
79            if ( $stmt instanceof PhpParser\Node\Expr\FuncCall ) {
80                $functionExpression = $stmt;
81            }
82            if ( isset( $stmt->expr ) && $stmt->expr instanceof PhpParser\Node\Expr\FuncCall ) {
83                $functionExpression = $stmt->expr;
84            }
85            if ( $functionExpression && $functionExpression->name->toString() === 'wfDeprecated' ) {
86                return true;
87            }
88            return false;
89        }
90    }
91
92    /** @inheritDoc */
93    public function enterNode( PhpParser\Node $node ) {
94        $retVal = parent::enterNode( $node );
95
96        if ( $node instanceof PhpParser\Node\Stmt\ClassLike ) {
97            $this->currentClass = $node->name;
98        }
99
100        if ( $node instanceof PhpParser\Node\FunctionLike ) {
101            $docComment = $node->getDocComment();
102            if ( !$docComment ) {
103                return;
104            }
105            if ( !preg_match( '/@deprecated.*(\d+\.\d+)/', $docComment->getText(), $matches ) ) {
106                return;
107            }
108            $version = $matches[1];
109
110            if ( $node instanceof PhpParser\Node\Stmt\ClassMethod ) {
111                $name = $this->currentClass . '::' . $node->name;
112            } else {
113                $name = $node->name;
114            }
115
116            $this->foundNodes[ $version ][] = [
117                'filename' => $node->filename,
118                'line'     => $node->getLine(),
119                'name'     => $name,
120                'hard'     => $this->isHardDeprecated( $node ),
121            ];
122        }
123
124        return $retVal;
125    }
126}
127
128/**
129 * Maintenance task that recursively scans MediaWiki PHP files for deprecated
130 * functions and interfaces and produces a report.
131 */
132class FindDeprecated extends Maintenance {
133    public function __construct() {
134        parent::__construct();
135        $this->addDescription( 'Find deprecated interfaces' );
136    }
137
138    /**
139     * @return string The installation path of MediaWiki. This method is mocked in PHPUnit tests.
140     */
141    protected function getMwInstallPath() {
142        return MW_INSTALL_PATH;
143    }
144
145    /**
146     * @return SplFileInfo[]
147     */
148    public function getFiles() {
149        $files = new RecursiveDirectoryIterator( $this->getMwInstallPath() . '/includes' );
150        $files = new RecursiveIteratorIterator( $files );
151        $files = new RegexIterator( $files, '/\.php$/' );
152        return iterator_to_array( $files, false );
153    }
154
155    public function execute() {
156        $files = $this->getFiles();
157        $chunkSize = (int)ceil( count( $files ) / 72 );
158
159        $parser = ( new PhpParser\ParserFactory )->createForVersion( PhpParser\PhpVersion::fromComponents( 7, 0 ) );
160        $traverser = new PhpParser\NodeTraverser;
161        $finder = new DeprecatedInterfaceFinder;
162        $traverser->addVisitor( $finder );
163
164        $fileCount = count( $files );
165
166        $outputProgress = !defined( 'MW_PHPUNIT_TEST' );
167
168        for ( $i = 0; $i < $fileCount; $i++ ) {
169            $file = $files[$i];
170            $code = file_get_contents( $file );
171
172            if ( !str_contains( $code, '@deprecated' ) ) {
173                continue;
174            }
175
176            $installPath = $this->getMwInstallPath();
177            $finder->setCurrentFile( substr( $file->getPathname(), strlen( $installPath ) + 1 ) );
178            $nodes = $parser->parse( $code );
179            $traverser->traverse( $nodes );
180
181            if ( $i % $chunkSize === 0 ) {
182                $percentDone = 100 * $i / $fileCount;
183                if ( $outputProgress ) {
184                    fprintf( STDERR, "\r[%-72s] %d%%", str_repeat( '#', $i / $chunkSize ), $percentDone );
185                }
186            }
187        }
188
189        if ( $outputProgress ) {
190            fprintf( STDERR, "\r[%'#-72s] 100%%\n", '' );
191        }
192
193        foreach ( $finder->getFoundNodes() as $version => $nodes ) {
194            echo "\n* Deprecated since $version:\n";
195            foreach ( $nodes as $node ) {
196                printf(
197                    "  %s %s (%s:%d)\n",
198                    $node['hard'] ? '+' : '-',
199                    $node['name'],
200                    $node['filename'],
201                    $node['line']
202                );
203            }
204        }
205        printf( "\nlegend:\n -: soft-deprecated\n +: hard-deprecated (via wfDeprecated())\n" );
206    }
207}
208
209// @codeCoverageIgnoreStart
210$maintClass = FindDeprecated::class;
211require_once RUN_MAINTENANCE_IF_MAIN;
212// @codeCoverageIgnoreEnd