Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH Correctly parse SomeClass::class syntax in textcollection #10577

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 71 additions & 5 deletions src/i18n/TextCollection/i18nTextCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -548,40 +548,83 @@ protected function getFileListForModule(Module $module)
*/
public function collectFromCode($content, $fileName, Module $module)
{
// Get namespace either from $fileName or $module fallback
// Get "namespace" either from $fileName or $module fallback
$namespace = $fileName ? basename($fileName) : $module->getName();

$usedFQCNs = [];
$entities = [];

$tokens = token_get_all("<?php\n" . $content);
$inTransFn = false;
$inConcat = false;
$inNamespace = false;
$inClass = false; // after `class` but before `{`
$inUse = false; // pulling in classes from other namespaces
$inArrayClosedBy = false; // Set to the expected closing token, or false if not in array
$inSelf = false; // Tracks progress of collecting self::class
$currentEntity = [];
$currentNameSpace = []; // The actual namespace for the current class
$currentClass = []; // Class components
$previousToken = null;
$thisToken = null; // used to populate $previousToken on next iteration
$potentialClassName = null;
$currentUse = null;
$currentUseAlias = null;
foreach ($tokens as $token) {
// Shuffle last token to $lastToken
$previousToken = $thisToken;
$thisToken = $token;
if (is_array($token)) {
list($id, $text) = $token;

// Collect use statements so we can get fully qualified class names
if ($id === T_USE) {
$inUse = true;
$currentUse = [];
continue;
}

if ($inUse) {
// PHP 8.0+
if (defined('T_NAME_QUALIFIED') && $id === T_NAME_QUALIFIED) {
$currentUse[] = $text;
$text = explode('\\', $text);
$currentUseAlias = end($text);
continue;
}
// PHP 7.4 or an alias declaration
if ($id === T_STRING) {
// Only add to the FQCN if it's the first string or comes after a namespace separator
if (empty($currentUse) || (is_array($previousToken) && $previousToken[0] === T_NS_SEPARATOR)) {
$currentUse[] = $text;
}
// The last part of the use statement is always the alias or the actual class name
$currentUseAlias = $text;
continue;
}
}

// Check class
if ($id === T_NAMESPACE) {
$inNamespace = true;
$currentClass = [];
$currentNameSpace = [];
continue;
}
if ($inNamespace && ($id === T_STRING || (defined('T_NAME_QUALIFIED') && $id === T_NAME_QUALIFIED))) {
$currentClass[] = $text;
$currentNameSpace[] = $text;
continue;
}

// This could be a ClassName::class declaration
if ($id === T_DOUBLE_COLON && is_array($previousToken) && $previousToken[0] === T_STRING) {
$prevString = $previousToken[1];
if (!in_array($prevString, ['self', 'static', 'parent'])) {
$potentialClassName = $prevString;
}
}

// Check class
if ($id === T_CLASS) {
// Skip if previous token was '::'. E.g. 'Object::class'
Expand All @@ -591,6 +634,16 @@ public function collectFromCode($content, $fileName, Module $module)
// for __CLASS__ to handle an array of class parts
$id = T_CLASS_C;
$inSelf = false;
} elseif ($potentialClassName) {
$id = T_CONSTANT_ENCAPSED_STRING;
if (array_key_exists($potentialClassName, $usedFQCNs)) {
// Handle classes that we explicitly know about from use statements
$text = "'" . $usedFQCNs[$potentialClassName] . "'";
} else {
// Assume the class is in the current namespace
$potentialFQCN = [...$currentNameSpace, $potentialClassName];
$text = "'" . implode('\\', $potentialFQCN) . "'";
}
} else {
// Don't handle other ::class definitions. We can't determine which
// class was invoked, so parent::class is not possible at this point.
Expand All @@ -600,7 +653,11 @@ public function collectFromCode($content, $fileName, Module $module)
$inClass = true;
continue;
}
} elseif (is_array($previousToken) && $previousToken[0] === T_DOUBLE_COLON) {
// We had a potential class but it turns out it was probably a method call.
$potentialClassName = null;
}

if ($inClass && $id === T_STRING) {
$currentClass[] = $text;
$inClass = false;
Expand Down Expand Up @@ -691,10 +748,19 @@ function ($input) {
continue;
}

// Check if we can close the namespace
if ($inNamespace && $token === ';') {
$inNamespace = false;
continue;
// Check if we can close the namespace or use statement
if ($token === ';') {
if ($inNamespace) {
$inNamespace = false;
continue;
}
if ($inUse) {
$inUse = false;
$usedFQCNs[$currentUseAlias] = implode('\\', $currentUse);
$currentUse = null;
$currentUseAlias = null;
continue;
}
}

// Continue only if in translation and not in array
Expand Down
58 changes: 58 additions & 0 deletions tests/php/i18n/i18nTextCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,64 @@ public function getMagicConstantStringFromSelf()
);
}

public function testCollectFromClassSyntax()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
<?php
namespace SilverStripe\Framework\Core;

use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
use Some\Space\MyClass as AliasClass;
use NoNamespaceClass;

class MyClass extends Base implements SomeService {
public function pointlessFunction1(\$class) {
if (
!is_subclass_of(\$class, DataObject::class)
|| !Object::has_extension(\$class, Versioned::class)
) {
return null;
}
return _t(
Versioned::class . '.OTHER_NAMESPACE',
'New Lines'
);
}
public function pointlessFunction2() {
return _t(
SameNamespaceClass::class . '.SAME_NAMESPACE',
'Slash=\\\\, Quote=\\''
);
}
public function pointlessFunction3() {
return _t(
AliasClass::class . ".ALIAS_CLASS",
"Slash=\\\\, Quote=\\""
);
}
public function pointlessFunction4()
{
return _t(
NoNamespaceClass::class . '.NO_NAMESPACE',
'Self Class'
);
}
}
PHP;

$this->assertEquals(
[
'SilverStripe\\Versioned\\Versioned.OTHER_NAMESPACE' => "New Lines",
'SilverStripe\\Framework\\Core\\SameNamespaceClass.SAME_NAMESPACE' => 'Slash=\\, Quote=\'',
'Some\\Space\\MyClass.ALIAS_CLASS' => 'Slash=\\, Quote="',
'NoNamespaceClass.NO_NAMESPACE' => 'Self Class',
],
$c->collectFromCode($php, null, $mymodule)
);
}

public function testNewlinesInEntityValues()
{
Expand Down