Follows-up I361fde0de7f4406bce6ed075ed397effa5be3359. Per T253461, not mass-changing source code, but the use of the native error silencing operator (@) is especially useful in tests because: 1. It requires any/all statements to be explicitly marked. The suppressWarnings/restoreWarnings sections encourage developers to be "lazy" and thus encapsulate more than needed if there are multiple ones near each other, which would ignore potentially important warnings in a test case, which is generally exactly the time when it is really useful to get warnings etc. 2. It avoids leaking state, for example in LBFactoryTest the assertFalse call would throw a PHPUnit assertion error (not meant to be caught by the local catch), and thus won't reach AtEase::restoreWarnings. This then causes later code to end up in a mismatching state and creates a confusing error_reporting state. See .phpcs.xml, where the at operator is allowed for all test code. Change-Id: I68d1725d685e0a7586468bc9de6dc29ceea31b8a
239 lines
7 KiB
PHP
239 lines
7 KiB
PHP
<?php
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Stmt;
|
|
use PhpParser\ParserFactory;
|
|
|
|
class AutoLoaderStructureTest extends MediaWikiIntegrationTestCase {
|
|
/**
|
|
* Assert that there were no classes loaded that are not registered with the AutoLoader.
|
|
*
|
|
* For example foo.php having class Foo and class Bar but only registering Foo.
|
|
* This is important because we should not be relying on Foo being used before Bar.
|
|
*/
|
|
public function testAutoLoadConfig() {
|
|
$results = self::checkAutoLoadConf();
|
|
|
|
$this->assertEquals(
|
|
$results['expected'],
|
|
$results['actual']
|
|
);
|
|
}
|
|
|
|
private static function parseFile( $contents ) {
|
|
// We could use token_get_all() here, but this is faster
|
|
// Note: Keep in sync with ClassCollector
|
|
$matches = [];
|
|
preg_match_all( '/
|
|
^ [\t ]* (?:
|
|
(?:final\s+)? (?:abstract\s+)? (?:class|interface|trait) \s+
|
|
(?P<class> \w+)
|
|
|
|
|
class_alias \s* \( \s*
|
|
([\'"]) (?P<original> [^\'"]+) \g{-2} \s* , \s*
|
|
([\'"]) (?P<alias> [^\'"]+ ) \g{-2} \s*
|
|
\) \s* ;
|
|
|
|
|
class_alias \s* \( \s*
|
|
(?P<originalStatic> [\w\\\\]+)::class \s* , \s*
|
|
([\'"]) (?P<aliasString> [^\'"]+ ) \g{-2} \s*
|
|
\) \s* ;
|
|
)
|
|
/imx', $contents, $matches, PREG_SET_ORDER );
|
|
|
|
$namespaceMatch = [];
|
|
preg_match( '/
|
|
^ [\t ]*
|
|
namespace \s+
|
|
(\w+(\\\\\w+)*)
|
|
\s* ;
|
|
/imx', $contents, $namespaceMatch );
|
|
$fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : '';
|
|
|
|
$classesInFile = [];
|
|
$aliasesInFile = [];
|
|
|
|
foreach ( $matches as $match ) {
|
|
if ( !empty( $match['class'] ) ) {
|
|
// 'class Foo {}'
|
|
$class = $fileNamespace . $match['class'];
|
|
$classesInFile[$class] = true;
|
|
} elseif ( !empty( $match['original'] ) ) {
|
|
// 'class_alias( "Foo", "Bar" );'
|
|
$aliasesInFile[self::removeSlashes( $match['alias'] )] = $match['original'];
|
|
} else {
|
|
// 'class_alias( Foo::class, "Bar" );'
|
|
$aliasesInFile[self::removeSlashes( $match['aliasString'] )] =
|
|
$fileNamespace . $match['originalStatic'];
|
|
}
|
|
}
|
|
|
|
return [ $classesInFile, $aliasesInFile ];
|
|
}
|
|
|
|
private static function removeSlashes( $str ) {
|
|
return str_replace( '\\\\', '\\', $str );
|
|
}
|
|
|
|
private static function fixSlashes( $str ) {
|
|
return str_replace( '\\', '/', $str );
|
|
}
|
|
|
|
protected static function checkAutoLoadConf() {
|
|
global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP;
|
|
|
|
// wgAutoloadLocalClasses has precedence, just like in includes/AutoLoader.php
|
|
$expected = $wgAutoloadLocalClasses + $wgAutoloadClasses;
|
|
$actual = [];
|
|
|
|
$psr4Namespaces = [];
|
|
foreach ( AutoLoader::getAutoloadNamespaces() as $ns => $path ) {
|
|
$psr4Namespaces[rtrim( $ns, '\\' ) . '\\'] = self::fixSlashes( rtrim( $path, '/' ) );
|
|
}
|
|
|
|
foreach ( $expected as $class => $file ) {
|
|
// Only prefix $IP if it doesn't have it already.
|
|
// Generally local classes don't have it, and those from extensions and test suites do.
|
|
if ( substr( $file, 0, 1 ) != '/' && substr( $file, 1, 1 ) != ':' ) {
|
|
$filePath = self::fixSlashes( "$IP/$file" );
|
|
} else {
|
|
$filePath = self::fixSlashes( $file );
|
|
}
|
|
|
|
if ( !is_file( $filePath ) ) {
|
|
$actual[$class] = "[file '$filePath' does not exist]";
|
|
continue;
|
|
}
|
|
|
|
$contents = @file_get_contents( $filePath );
|
|
if ( $contents === false ) {
|
|
$actual[$class] = "[couldn't read file '$filePath']";
|
|
continue;
|
|
}
|
|
|
|
list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents );
|
|
|
|
foreach ( $classesInFile as $className => $ignore ) {
|
|
// Skip if it's a PSR4 class
|
|
$parts = explode( '\\', $className );
|
|
for ( $i = count( $parts ) - 1; $i > 0; $i-- ) {
|
|
$ns = implode( '\\', array_slice( $parts, 0, $i ) ) . '\\';
|
|
if ( isset( $psr4Namespaces[$ns] ) ) {
|
|
$expectedPath = $psr4Namespaces[$ns] . '/'
|
|
. implode( '/', array_slice( $parts, $i ) )
|
|
. '.php';
|
|
if ( $filePath === $expectedPath ) {
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nope, add it.
|
|
$actual[$className] = $file;
|
|
}
|
|
|
|
// Only accept aliases for classes in the same file, because for correct
|
|
// behavior, all aliases for a class must be set up when the class is loaded
|
|
// (see <https://bugs.php.net/bug.php?id=61422>).
|
|
foreach ( $aliasesInFile as $alias => $class ) {
|
|
if ( isset( $classesInFile[$class] ) ) {
|
|
$actual[$alias] = $file;
|
|
} else {
|
|
$actual[$alias] = "[original class not in $file]";
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'expected' => $expected,
|
|
'actual' => $actual,
|
|
];
|
|
}
|
|
|
|
public function testAutoloadOrder() {
|
|
$path = __DIR__ . '/../../..';
|
|
$oldAutoload = file_get_contents( $path . '/autoload.php' );
|
|
$generator = new AutoloadGenerator( $path, 'local' );
|
|
$generator->setPsr4Namespaces( AutoLoader::getAutoloadNamespaces() );
|
|
$generator->initMediaWikiDefault();
|
|
$newAutoload = $generator->getAutoload( 'maintenance/generateLocalAutoload.php' );
|
|
|
|
$this->assertEquals( $oldAutoload, $newAutoload, 'autoload.php does not match' .
|
|
' output of generateLocalAutoload.php script.' );
|
|
}
|
|
|
|
/**
|
|
* Verify that all the directories specified for PSR-4 autoloading
|
|
* actually exist, to prevent situations like T259448
|
|
*/
|
|
public function testAutoloadNamespaces() {
|
|
$missing = [];
|
|
foreach ( AutoLoader::$psr4Namespaces as $ns => $path ) {
|
|
if ( !is_dir( $path ) ) {
|
|
$missing[] = "Directory $path for namespace $ns does not exist";
|
|
}
|
|
}
|
|
|
|
$this->assertSame( [], $missing );
|
|
}
|
|
|
|
public static function provideAutoloadNoFileScope() {
|
|
global $wgAutoloadLocalClasses;
|
|
$files = array_unique( $wgAutoloadLocalClasses );
|
|
$args = [];
|
|
foreach ( $files as $file ) {
|
|
$args[$file] = [ $file ];
|
|
}
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Confirm that all files in $wgAutoloadLocalClasses have no file-scope code
|
|
* apart from specific exemptions.
|
|
*
|
|
* This is slow (~15s). Running it arguably renders all the performance
|
|
* optimisations above obsolete.
|
|
*
|
|
* @dataProvider provideAutoloadNoFileScope
|
|
*/
|
|
public function testAutoloadNoFileScope( $file ) {
|
|
$parser = ( new ParserFactory )->create( ParserFactory::ONLY_PHP7 );
|
|
$ast = $parser->parse( file_get_contents( $file ) );
|
|
foreach ( $ast as $node ) {
|
|
if ( $node instanceof Stmt\ClassLike
|
|
|| $node instanceof Stmt\Namespace_
|
|
|| $node instanceof Stmt\Use_
|
|
|| $node instanceof Stmt\Nop
|
|
|| $node instanceof Stmt\Declare_
|
|
|| $node instanceof Stmt\Function_
|
|
) {
|
|
continue;
|
|
}
|
|
if ( $node instanceof Stmt\Expression ) {
|
|
$expr = $node->expr;
|
|
if ( $expr instanceof Expr\FuncCall ) {
|
|
if ( $expr->name instanceof Node\Name ) {
|
|
if ( in_array( $expr->name->toString(), [
|
|
'class_alias',
|
|
'define'
|
|
] ) ) {
|
|
continue;
|
|
}
|
|
}
|
|
} elseif ( $expr instanceof Expr\Include_ ) {
|
|
if ( $expr->type === Expr\Include_::TYPE_REQUIRE_ONCE ) {
|
|
continue;
|
|
}
|
|
} elseif ( $expr instanceof Expr\Assign ) {
|
|
if ( $expr->var->name === 'maintClass' ) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
$line = $node->getLine();
|
|
$this->assertNull( $node, "Found file scope code in $file at line $line" );
|
|
}
|
|
$this->assertTrue( true );
|
|
}
|
|
}
|