Skip to content

Commit

Permalink
NEW Add rule to catch malformed _t() calls
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Aug 5, 2024
1 parent ee6a851 commit c5d433a
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
rules:
- SilverStripe\Standards\PHPStan\MethodAnnotationsRule
- SilverStripe\Standards\PHPStan\KeywordSelfRule
- SilverStripe\Standards\PHPStan\TranslationFunctionRule

parameters:
# Setting customRulestUsed to true allows us to avoid using the built-in rules
Expand Down
96 changes: 96 additions & 0 deletions src/PHPStan/TranslationFunctionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace SilverStripe\Standards\PHPStan;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantStringType;

/**
* Validates that the first argument to the _t() function isn't malformed.
*
* See https://phpstan.org/developing-extensions/rules
*
* @implements Rule<FuncCall|StaticCall>
*/
class TranslationFunctionRule implements Rule
{
public function getNodeType(): string
{
return Node::class;
}

public function processNode(Node $node, Scope $scope): array
{
if ($node instanceof FuncCall) {
return $this->processFuncCall($node, $scope);
}
if ($node instanceof StaticCall) {
return $this->processStaticCall($node, $scope);
}
return [];
}

/**
* Process calls to the global function _t()
*/
private function processFuncCall(FuncCall $node, Scope $scope): array
{
if ($node->name->__toString() !== '_t') {
return [];
}
return $this->processArgs($node->getArgs(), $scope);
}

/**
* Process calls to the static method i18n::_t()
*/
private function processStaticCall(StaticCall $node, Scope $scope): array
{
$class = $node->class;
if (!($class instanceof Name)) {
return [];
}
if ($class->toString() !== 'SilverStripe\i18n\i18n') {
return [];
}
return $this->processArgs($node->getArgs(), $scope);
}

/**
* Check that the first arg value can be evaluated and has exactly one period.
*
* @param Arg[] $args
*/
private function processArgs(array $args, Scope $scope): array
{
// If we have no args PHP itself will complain and it'll be caught by other linting, so just skip.
if (count($args) < 1) {
return [];
}

$entityArg = $scope->getType($args[0]->value);
// If phpstan can't get us a nice clear value, text collector almost certainly can't either.
if (!($entityArg instanceof ConstantStringType)) {
return [
RuleErrorBuilder::message(
'Can\'t determine value of first argument to _t(). Use a simpler value.'
)->build()
];
}

if (substr_count($entityArg->getValue(), '.') !== 1) {
return [RuleErrorBuilder::message('First argument passed to _t() must have exactly one period.')->build()];
}

return [];
}
}
58 changes: 58 additions & 0 deletions tests/PHPStan/TranslationFunctionRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace SilverStripe\Standards\Tests\PHPStan;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use SilverStripe\Standards\PHPStan\TranslationFunctionRule;

/**
* @extends RuleTestCase<TranslationFunctionRule>
*/
class TranslationFunctionRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new TranslationFunctionRule();
}

public function provideRule()
{
return [
'no class scope' => [
'filePaths' => [__DIR__ . '/TranslationFunctionRuleTest/raw-php.php'],
'errorMessage' => 'First argument passed to _t() must have exactly one period.',
'errorLines' => [8,9,10,11,12,14,15],
],
'no class scope, no errors' => [
'filePaths' => [__DIR__ . '/TranslationFunctionRuleTest/raw-php-correct.php'],
'errorMessage' => '',
'errorLines' => [],
],
'in class scope' => [
'filePaths' => [__DIR__ . '/TranslationFunctionRuleTest/InClass.php'],
'errorMessage' => 'First argument passed to _t() must have exactly one period.',
'errorLines' => [13,14,15,16,17,19,20],
],
'in class scope, no errors' => [
'filePaths' => [__DIR__ . '/TranslationFunctionRuleTest/InClassCorrect.php'],
'errorMessage' => '',
'errorLines' => [],
],
];
}

/**
* @dataProvider provideRule
*/
public function testRule(array $filePaths, string $errorMessage, array $errorLines): void
{
$errors = [];
foreach ($errorLines as $line) {
$errors[] = [$errorMessage, $line];
}
$this->analyse($filePaths, $errors);
}
}
27 changes: 27 additions & 0 deletions tests/PHPStan/TranslationFunctionRuleTest/InClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace SilverStripe\Standards\Tests\PHPStan\TranslationFunctionRuleTest;

use SilverStripe\i18n\i18n;

class InClass
{
public const MY_ENTITY = 'entity';

public function myMethod()
{
_t('abc');
_t(InClass::class);
_t(InClass::class . 'somekey');
_t(InClass::MY_ENTITY);
_t(__CLASS__ . InClass::MY_ENTITY);
$variable = 'somethingwrong';
_t($variable);
i18n::_t('nodot');

// These should be ignored
SomeClass::_t('abc');
$x = new SomeClass();
$x->_t('abc');
}
}
21 changes: 21 additions & 0 deletions tests/PHPStan/TranslationFunctionRuleTest/InClassCorrect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace SilverStripe\Standards\Tests\PHPStan\TranslationFunctionRuleTest;

use SilverStripe\i18n\i18n;

class InClassCorrect
{
public const MY_ENTITY = 'entity';

public function myMethod()
{
_t('abc.123');
_t(InClass::class . '.somekey');
_t('something.' . InClass::MY_ENTITY);
_t(__CLASS__ . '.' . InClass::MY_ENTITY);
$variable = 'nothing.wrong';
_t($variable);
i18n::_t('with.dot');
}
}
14 changes: 14 additions & 0 deletions tests/PHPStan/TranslationFunctionRuleTest/raw-php-correct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

use App\SomeClass;
use SilverStripe\i18n\i18n;

const MY_ENTITY = 'entity';

_t('abc.123');
_t(SomeClass::class . '.somekey');
_t('something.' . MY_ENTITY);
_t(SomeClass::class . '.' . MY_ENTITY);
$variable = 'nothing.wrong';
_t($variable);
i18n::_t('with.dot');
20 changes: 20 additions & 0 deletions tests/PHPStan/TranslationFunctionRuleTest/raw-php.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use App\SomeClass;
use SilverStripe\i18n\i18n;

const MY_ENTITY = 'entity';

_t('abc');
_t(SomeClass::class);
_t(SomeClass::class . 'somekey');
_t(MY_ENTITY);
_t(SomeClass::class . MY_ENTITY);
$variable = 'somethingwrong';
_t($variable);
i18n::_t('nodot');

// These should be ignored
SomeClass::_t('abc');
$x = new SomeClass();
$x->_t('abc');

0 comments on commit c5d433a

Please sign in to comment.