Rewrite ErrorReporter for performance and separation of concerns

This patch is mostly moving code around without changing the behavior.
Exceptions:

* The ErrorReporter creates a <span> container. This was previously
parsed. The only benefit might be error checking and escaping. Rather
pointless. The code just created this HTML. With this patch, it is not
parsed any more. The unit test reflects this change. The output in
production will not change, as the parser tests show.

* Parsing of the message key (to detect it's type and id) is simplified
a lot, using explode. With this the code can, in theory, support more
types.

Bug: T239572
Change-Id: If2fe5f55db46dfc7e0ce445348608bef00bec64e
This commit is contained in:
Thiemo Kreuz 2020-01-28 17:40:27 +01:00 committed by Adam Wight
parent 8105f64740
commit d80bd3ef97
2 changed files with 81 additions and 40 deletions

View file

@ -4,6 +4,7 @@ namespace Cite;
use Html; use Html;
use Language; use Language;
use Message;
use Parser; use Parser;
use Sanitizer; use Sanitizer;
@ -25,9 +26,7 @@ class ErrorReporter {
/** /**
* @param ReferenceMessageLocalizer $messageLocalizer * @param ReferenceMessageLocalizer $messageLocalizer
*/ */
public function __construct( public function __construct( ReferenceMessageLocalizer $messageLocalizer ) {
ReferenceMessageLocalizer $messageLocalizer
) {
$this->messageLocalizer = $messageLocalizer; $this->messageLocalizer = $messageLocalizer;
} }
@ -39,8 +38,9 @@ class ErrorReporter {
* @return string Half-parsed wikitext with extension's tags already being expanded * @return string Half-parsed wikitext with extension's tags already being expanded
*/ */
public function halfParsed( Parser $parser, string $key, ...$params ) : string { public function halfParsed( Parser $parser, string $key, ...$params ) : string {
// FIXME: We suspect this is not necessary and can just be removed $msg = $this->msg( $parser, $key, ...$params );
return $parser->recursiveTagParse( $this->plain( $parser, $key, ...$params ) ); $wikitext = $parser->recursiveTagParse( $msg->plain() );
return $this->wrapInHtmlContainer( $wikitext, $key, $msg->getLanguage() );
} }
/** /**
@ -52,31 +52,31 @@ class ErrorReporter {
* @return-taint tainted * @return-taint tainted
*/ */
public function plain( Parser $parser, string $key, ...$params ) : string { public function plain( Parser $parser, string $key, ...$params ) : string {
$interfaceLanguage = $this->getInterfaceLanguageAndSplitCache( $parser ); $msg = $this->msg( $parser, $key, ...$params );
$msg = $this->messageLocalizer->msg( $key, ...$params )->inLanguage( $interfaceLanguage ); $wikitext = $msg->plain();
return $this->wrapInHtmlContainer( $wikitext, $key, $msg->getLanguage() );
}
if ( strncmp( $msg->getKey(), 'cite_warning_', 13 ) === 0 ) { /**
$type = 'warning'; * @param Parser $parser
$id = substr( $msg->getKey(), 13 ); * @param string $key
$extraClass = ' mw-ext-cite-warning-' . Sanitizer::escapeClass( $id ); * @param mixed ...$params
} else { *
$type = 'error'; * @return Message
$extraClass = ''; */
private function msg( Parser $parser, string $key, ...$params ) : Message {
$language = $this->getInterfaceLanguageAndSplitCache( $parser );
$msg = $this->messageLocalizer->msg( $key, ...$params )->inLanguage( $language );
[ $type, ] = $this->parseTypeAndIdFromMessageKey( $msg->getKey() );
if ( $type === 'error' ) {
// Take care; this is a sideeffect that might not belong to this class. // Take care; this is a sideeffect that might not belong to this class.
$parser->addTrackingCategory( 'cite-tracking-category-cite-error' ); $parser->addTrackingCategory( 'cite-tracking-category-cite-error' );
} }
return Html::rawElement( // Messages: cite_error, cite_warning
'span', return $this->messageLocalizer->msg( "cite_$type", $msg->plain() )->inLanguage( $language );
[
'class' => "$type mw-ext-cite-$type" . $extraClass,
'lang' => $interfaceLanguage->getHtmlCode(),
'dir' => $interfaceLanguage->getDir(),
],
$this->messageLocalizer->msg( "cite_$type", $msg->plain() )
->inLanguage( $interfaceLanguage )->plain()
);
} }
/** /**
@ -93,4 +93,41 @@ class ErrorReporter {
return $this->cachedInterfaceLanguage; return $this->cachedInterfaceLanguage;
} }
/**
* @param string $wikitext
* @param string $key
* @param Language $language
*
* @return string
*/
private function wrapInHtmlContainer(
string $wikitext,
string $key,
Language $language
) : string {
[ $type, $id ] = $this->parseTypeAndIdFromMessageKey( $key );
$extraClass = $type === 'warning'
? ' mw-ext-cite-warning-' . Sanitizer::escapeClass( $id )
: '';
return Html::rawElement(
'span',
[
'class' => "$type mw-ext-cite-$type" . $extraClass,
'lang' => $language->getHtmlCode(),
'dir' => $language->getDir(),
],
$wikitext
);
}
/**
* @param string $messageKey Expected to be a message key like "cite_error_ref_too_many_keys"
*
* @return string[]
*/
private function parseTypeAndIdFromMessageKey( string $messageKey ) : array {
return array_slice( explode( '_', $messageKey, 3 ), 1 );
}
} }

View file

@ -27,8 +27,9 @@ class ErrorReporterTest extends \MediaWikiUnitTestCase {
string $expectedHtml, string $expectedHtml,
array $expectedCategories array $expectedCategories
) { ) {
$reporter = $this->createReporter(); $language = $this->createLanguage();
$mockParser = $this->createParser( $expectedCategories ); $reporter = $this->createReporter( $language );
$mockParser = $this->createParser( $language, $expectedCategories );
$this->assertSame( $this->assertSame(
$expectedHtml, $expectedHtml,
$reporter->plain( $mockParser, $key, 'first param' ) ); $reporter->plain( $mockParser, $key, 'first param' ) );
@ -38,11 +39,12 @@ class ErrorReporterTest extends \MediaWikiUnitTestCase {
* @covers ::halfParsed * @covers ::halfParsed
*/ */
public function testHalfParsed() { public function testHalfParsed() {
$reporter = $this->createReporter(); $language = $this->createLanguage();
$mockParser = $this->createParser( [] ); $reporter = $this->createReporter( $language );
$mockParser = $this->createParser( $language, [] );
$this->assertSame( $this->assertSame(
'[<span class="warning mw-ext-cite-warning mw-ext-cite-warning-example" lang="qqx" ' . '<span class="warning mw-ext-cite-warning mw-ext-cite-warning-example" lang="qqx" ' .
'dir="rtl">(cite_warning|(cite_warning_example|first param))</span>]', 'dir="rtl">[(cite_warning|(cite_warning_example|first param))]</span>',
$reporter->halfParsed( $mockParser, 'cite_warning_example', 'first param' ) ); $reporter->halfParsed( $mockParser, 'cite_warning_example', 'first param' ) );
} }
@ -63,16 +65,22 @@ class ErrorReporterTest extends \MediaWikiUnitTestCase {
]; ];
} }
private function createReporter() : ErrorReporter { private function createLanguage() : Language {
$language = $this->createMock( Language::class );
$language->method( 'getDir' )->willReturn( 'rtl' );
$language->method( 'getHtmlCode' )->willReturn( 'qqx' );
return $language;
}
private function createReporter( Language $language ) : ErrorReporter {
$mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class ); $mockMessageLocalizer = $this->createMock( ReferenceMessageLocalizer::class );
$mockMessageLocalizer->method( 'msg' )->willReturnCallback( $mockMessageLocalizer->method( 'msg' )->willReturnCallback(
function ( ...$args ) { function ( ...$args ) use ( $language ) {
$message = $this->createMock( Message::class ); $message = $this->createMock( Message::class );
$message->method( 'getKey' )->willReturn( $args[0] ); $message->method( 'getKey' )->willReturn( $args[0] );
$message->method( 'plain' )->willReturn( '(' . implode( '|', $args ) . ')' ); $message->method( 'plain' )->willReturn( '(' . implode( '|', $args ) . ')' );
$message->method( 'inLanguage' )->with( $this->callback( function ( Language $lang ) { $message->method( 'inLanguage' )->with( $language )->willReturnSelf();
return $lang->getHtmlCode() === 'qqx'; $message->method( 'getLanguage' )->willReturn( $language );
} ) )->willReturnSelf();
return $message; return $message;
} }
); );
@ -81,11 +89,7 @@ class ErrorReporterTest extends \MediaWikiUnitTestCase {
return new ErrorReporter( $mockMessageLocalizer ); return new ErrorReporter( $mockMessageLocalizer );
} }
public function createParser( array $expectedCategories ) { public function createParser( Language $language, array $expectedCategories ) {
$language = $this->createMock( Language::class );
$language->method( 'getDir' )->willReturn( 'rtl' );
$language->method( 'getHtmlCode' )->willReturn( 'qqx' );
$parserOptions = $this->createMock( ParserOptions::class ); $parserOptions = $this->createMock( ParserOptions::class );
$parserOptions->method( 'getUserLangObj' )->willReturn( $language ); $parserOptions->method( 'getUserLangObj' )->willReturn( $language );