From ae50f5ae1572c79f81b03980badef63c9aa6dd3a Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Tue, 6 Aug 2024 11:18:37 +1200 Subject: [PATCH] NEW Add rule to catch malformed _t() calls --- rules.neon | 1 + src/PHPStan/TranslationFunctionRule.php | 96 +++++++++++++++++++ tests/PHPStan/TranslationFunctionRuleTest.php | 58 +++++++++++ .../TranslationFunctionRuleTest/InClass.php | 27 ++++++ .../InClassCorrect.php | 21 ++++ .../raw-php-correct.php | 14 +++ .../TranslationFunctionRuleTest/raw-php.php | 20 ++++ 7 files changed, 237 insertions(+) create mode 100644 src/PHPStan/TranslationFunctionRule.php create mode 100644 tests/PHPStan/TranslationFunctionRuleTest.php create mode 100644 tests/PHPStan/TranslationFunctionRuleTest/InClass.php create mode 100644 tests/PHPStan/TranslationFunctionRuleTest/InClassCorrect.php create mode 100644 tests/PHPStan/TranslationFunctionRuleTest/raw-php-correct.php create mode 100644 tests/PHPStan/TranslationFunctionRuleTest/raw-php.php diff --git a/rules.neon b/rules.neon index 56a6367..827b7ee 100644 --- a/rules.neon +++ b/rules.neon @@ -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 diff --git a/src/PHPStan/TranslationFunctionRule.php b/src/PHPStan/TranslationFunctionRule.php new file mode 100644 index 0000000..2b1d127 --- /dev/null +++ b/src/PHPStan/TranslationFunctionRule.php @@ -0,0 +1,96 @@ + + */ +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 []; + } +} diff --git a/tests/PHPStan/TranslationFunctionRuleTest.php b/tests/PHPStan/TranslationFunctionRuleTest.php new file mode 100644 index 0000000..0dada4d --- /dev/null +++ b/tests/PHPStan/TranslationFunctionRuleTest.php @@ -0,0 +1,58 @@ + + */ +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); + } +} diff --git a/tests/PHPStan/TranslationFunctionRuleTest/InClass.php b/tests/PHPStan/TranslationFunctionRuleTest/InClass.php new file mode 100644 index 0000000..885bb23 --- /dev/null +++ b/tests/PHPStan/TranslationFunctionRuleTest/InClass.php @@ -0,0 +1,27 @@ +_t('abc'); + } +} diff --git a/tests/PHPStan/TranslationFunctionRuleTest/InClassCorrect.php b/tests/PHPStan/TranslationFunctionRuleTest/InClassCorrect.php new file mode 100644 index 0000000..f9621dc --- /dev/null +++ b/tests/PHPStan/TranslationFunctionRuleTest/InClassCorrect.php @@ -0,0 +1,21 @@ +_t('abc');