diff --git a/.travis.yml b/.travis.yml index 8435b1a..f402337 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,8 @@ language: php php: - 7.3 - 7.2 -before_script: +install: - composer install --no-interaction --prefer-dist +script: + - ./vendor/bin/phpunit + - ./vendor/bin/phpstan analyse diff --git a/composer.json b/composer.json index 833e60d..783edf4 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "require": { "php": ">=7.2.0", "nikic/php-parser": "~4.2.0", - "latte/latte": "~2.5.0" + "latte/latte": "~2.5.0", + "nette/utils": "^3.0" }, "autoload": { "psr-4": {"Vodacek\\GettextExtractor\\" : "src"} @@ -25,6 +26,8 @@ "gettext-extractor.php" ], "require-dev": { - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^8", + "phpstan/phpstan-shim": "^0.11.5", + "phpstan/phpstan-phpunit": "^0.11.0" } } diff --git a/gettext-extractor.php b/gettext-extractor.php index efbf897..8731264 100755 --- a/gettext-extractor.php +++ b/gettext-extractor.php @@ -75,9 +75,9 @@ $keywords[] = array( 'filter' => $filter, 'function' => $function, - 'singular' => isset($params[0]) ? $params[0] : null, - 'plural' => isset($params[1]) ? $params[1] : null, - 'context' => isset($params[2]) ? $params[2] : null + 'singular' => isset($params[0]) ? (int) $params[0] : null, + 'plural' => isset($params[1]) ? (int) $params[1] : null, + 'context' => isset($params[2]) ? (int) $params[2] : null ); } } diff --git a/makefile b/makefile new file mode 100644 index 0000000..5804cfd --- /dev/null +++ b/makefile @@ -0,0 +1,7 @@ +check: test phpstan + +test: + ./vendor/bin/phpunit + +phpstan: + ./vendor/bin/phpstan analyse diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..062f63a --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,13 @@ +parameters: + level: 7 + paths: + - src + - tests +# - gettext-extractor.php + excludes_analyse: + - tests/integration/data + - tests/unit/data + +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon diff --git a/src/Extractor.php b/src/Extractor.php index c27fc25..1924d81 100644 --- a/src/Extractor.php +++ b/src/Extractor.php @@ -8,6 +8,7 @@ namespace Vodacek\GettextExtractor; +use Nette\Utils\FileSystem; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RuntimeException; @@ -83,7 +84,7 @@ protected function throwException(string $message): void { /** * Scans given files or directories and extracts gettext keys from the content * - * @param string|array $resource + * @param string|string[] $resource * @return self */ public function scan($resource): self { @@ -237,7 +238,7 @@ public function setMeta(string $key, string $value): self { * @return self */ public function save(string $outputFile, array $data = null): self { - file_put_contents($outputFile, $this->formatData($data ?: $this->data)); + FileSystem::write($outputFile, $this->formatData($data ?: $this->data)); return $this; } diff --git a/src/Filters/AFilter.php b/src/Filters/AFilter.php index 33d081e..87a7c76 100644 --- a/src/Filters/AFilter.php +++ b/src/Filters/AFilter.php @@ -18,10 +18,10 @@ abstract class AFilter { /** * Includes a function to parse gettext phrases from * - * @param $functionName string - * @param $singular int - * @param $plural int|null - * @param $context int|null + * @param string $functionName + * @param int $singular + * @param int|null $plural + * @param int|null $context * @return self */ public function addFunction(string $functionName, int $singular = 1, int $plural = null, int $context = null): self { @@ -50,7 +50,7 @@ public function addFunction(string $functionName, int $singular = 1, int $plural /** * Excludes a function from the function list * - * @param $functionName + * @param string $functionName * @return self */ public function removeFunction(string $functionName): self { diff --git a/src/Filters/LatteFilter.php b/src/Filters/LatteFilter.php index a61d076..1716cf4 100644 --- a/src/Filters/LatteFilter.php +++ b/src/Filters/LatteFilter.php @@ -8,6 +8,10 @@ namespace Vodacek\GettextExtractor\Filters; +use Nette\Utils\FileSystem; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\Expression; use Vodacek\GettextExtractor\Extractor; use PhpParser; use Latte; @@ -29,7 +33,7 @@ public function extract(string $file): array { $data = array(); $latteParser = new Latte\Parser(); - $tokens = $latteParser->parse(file_get_contents($file)); + $tokens = $latteParser->parse(FileSystem::read($file)); $functions = array_keys($this->functions); usort($functions, static function(string $a, string $b) { @@ -49,25 +53,30 @@ public function extract(string $file): array { $value = $this->trimMacroValue($name, $token->value); $stmts = $phpParser->parse("functions[$name] as $definition) { - $message = $this->processFunction($definition, $stmts[0]->expr); - if ($message) { - $message[Extractor::LINE] = $token->line; - $data[] = $message; + if ($stmts === null) { + continue; + } + if ($stmts[0] instanceof Expression && $stmts[0]->expr instanceof FuncCall) { + foreach ($this->functions[$name] as $definition) { + $message = $this->processFunction($definition, $stmts[0]->expr); + if ($message) { + $message[Extractor::LINE] = $token->line; + $data[] = $message; + } } } } return $data; } - private function processFunction(array $definition, PhpParser\Node\Expr\FuncCall $node): array { + private function processFunction(array $definition, FuncCall $node): array { $message = []; foreach ($definition as $type => $position) { if (!isset($node->args[$position - 1])) { return []; } $arg = $node->args[$position - 1]->value; - if ($arg instanceof PhpParser\Node\Scalar\String_) { + if ($arg instanceof String_) { $message[$type] = $arg->value; } else { return []; diff --git a/src/Filters/PHPFilter.php b/src/Filters/PHPFilter.php index 31297ce..e155b0b 100644 --- a/src/Filters/PHPFilter.php +++ b/src/Filters/PHPFilter.php @@ -7,13 +7,23 @@ namespace Vodacek\GettextExtractor\Filters; +use Nette\Utils\FileSystem; use PhpParser; +use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; use Vodacek\GettextExtractor\Extractor; class PHPFilter extends AFilter implements IFilter, PhpParser\NodeVisitor { /** @var array */ - private $data; + private $data = []; public function __construct() { $this->addFunction('gettext', 1); @@ -29,47 +39,58 @@ public function __construct() { public function extract(string $file): array { $this->data = array(); $parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::PREFER_PHP7); - $stmts = $parser->parse(file_get_contents($file)); + $stmts = $parser->parse(FileSystem::read($file)); + if ($stmts === null) { + return []; + } $traverser = new PhpParser\NodeTraverser(); $traverser->addVisitor($this); $traverser->traverse($stmts); $data = $this->data; - $this->data = null; + $this->data = []; return $data; } - public function enterNode(PhpParser\Node $node) { + public function enterNode(Node $node) { $name = null; - if (($node instanceof PhpParser\Node\Expr\MethodCall || $node instanceof PhpParser\Node\Expr\StaticCall) && $node->name instanceof PhpParser\Node\Identifier && is_string($node->name->name)) { + $args = []; + if (($node instanceof MethodCall || $node instanceof StaticCall) && $node->name instanceof Identifier && is_string($node->name->name)) { $name = $node->name->name; - } elseif ($node instanceof PhpParser\Node\Expr\FuncCall && $node->name instanceof PhpParser\Node\Name) { + $args = $node->args; + } elseif ($node instanceof FuncCall && $node->name instanceof Name) { $parts = $node->name->parts; $name = array_pop($parts); + $args = $node->args; } else { - return; + return null; } if (!isset($this->functions[$name])) { - return; + return null; } foreach ($this->functions[$name] as $definition) { - $this->processFunction($definition, $node); + $this->processFunction($definition, $node, $args); } } - private function processFunction(array $definition, PhpParser\Node $node) { + /** + * @param array $definition + * @param Node $node + * @param Arg[] $args + */ + private function processFunction(array $definition, Node $node, array $args) { $message = array( Extractor::LINE => $node->getLine() ); foreach ($definition as $type => $position) { - if (!isset($node->args[$position - 1])) { + if (!isset($args[$position - 1])) { return; } - $arg = $node->args[$position - 1]->value; - if ($arg instanceof PhpParser\Node\Scalar\String_) { + $arg = $args[$position - 1]->value; + if ($arg instanceof String_) { $message[$type] = $arg->value; - } elseif ($arg instanceof PhpParser\Node\Expr\Array_) { + } elseif ($arg instanceof Array_) { foreach ($arg->items as $item) { - if ($item->value instanceof PhpParser\Node\Scalar\String_) { + if ($item->value instanceof String_) { $message[$type][] = $item->value->value; } } @@ -99,6 +120,6 @@ public function afterTraverse(array $nodes) { public function beforeTraverse(array $nodes) { } - public function leaveNode(PhpParser\Node $node) { + public function leaveNode(Node $node) { } } diff --git a/src/NetteExtractor.php b/src/NetteExtractor.php index f25b65a..3a8caff 100644 --- a/src/NetteExtractor.php +++ b/src/NetteExtractor.php @@ -8,12 +8,12 @@ namespace Vodacek\GettextExtractor; +use Vodacek\GettextExtractor\Filters\LatteFilter; +use Vodacek\GettextExtractor\Filters\PHPFilter; + class NetteExtractor extends Extractor { - /** - * @param string|bool $logToFile - */ - public function __construct($logToFile = false) { + public function __construct(string $logToFile = 'php://stderr') { parent::__construct($logToFile); // Clean up... @@ -28,11 +28,15 @@ public function __construct($logToFile = false) { $this->addFilter('Latte', new Filters\LatteFilter()); - $this->getFilter('PHP') - ->addFunction('translate'); + $phpFilter = $this->getFilter('PHP'); + assert($phpFilter instanceof PHPFilter); + + $phpFilter->addFunction('translate'); - $this->getFilter('Latte') - ->addFunction('!_') + $latteFilter = $this->getFilter('Latte'); + assert($latteFilter instanceof LatteFilter); + + $latteFilter->addFunction('!_') ->addFunction('_'); } @@ -43,6 +47,8 @@ public function __construct($logToFile = false) { */ public function setupForms(): self { $php = $this->getFilter('PHP'); + assert($php instanceof PHPFilter); + $php->addFunction('setText') ->addFunction('setEmptyValue') ->addFunction('setValue') @@ -80,6 +86,8 @@ public function setupForms(): self { */ public function setupDataGrid(): self { $php = $this->getFilter('PHP'); + assert($php instanceof PHPFilter); + $php->addFunction('addColumn', 2) ->addFunction('addNumericColumn', 2) ->addFunction('addDateColumn', 2) diff --git a/tests/integration/NetteFormsTest.php b/tests/integration/NetteFormsTest.php index 3ab723c..bf4a583 100644 --- a/tests/integration/NetteFormsTest.php +++ b/tests/integration/NetteFormsTest.php @@ -20,7 +20,11 @@ public function testForm(): void { $this->object->setupForms(); $this->object->scan('tests/integration/data/form.php'); $temp = tempnam(sys_get_temp_dir(), __CLASS__); + if ($temp === false) { + throw new \RuntimeException('Failed to create temporary file.'); + } $this->object->save($temp); $this->assertFileEquals(__DIR__.'/data/form.pot', $temp); + unlink($temp); } } diff --git a/tests/unit/GettextExtractor/Filters/AFilterTest.php b/tests/unit/GettextExtractor/Filters/AFilterTest.php index 6c07f3a..f46abfe 100644 --- a/tests/unit/GettextExtractor/Filters/AFilterTest.php +++ b/tests/unit/GettextExtractor/Filters/AFilterTest.php @@ -1,15 +1,16 @@ object->extract($this->file); - - $this->assertIsArray($messages); - - $this->assertContains(array( - GE\Extractor::LINE => 2, - GE\Extractor::SINGULAR => 'A message!' - ), $messages); - - $this->assertContains(array( - GE\Extractor::LINE => 3, - GE\Extractor::SINGULAR => 'Another message!', - GE\Extractor::CONTEXT => 'context' - ), $messages); - - $this->assertContains(array( - GE\Extractor::LINE => 4, - GE\Extractor::SINGULAR => 'I see %d little indian!', - GE\Extractor::PLURAL => 'I see %d little indians!' - ), $messages); - - $this->assertContains(array( - GE\Extractor::LINE => 5, - GE\Extractor::SINGULAR => 'I see %d little indian!', - GE\Extractor::PLURAL => 'I see %d little indians!', - GE\Extractor::CONTEXT => 'context' - ), $messages); - } -} diff --git a/tests/unit/GettextExtractor/Filters/LatteFilterTest.php b/tests/unit/GettextExtractor/Filters/LatteFilterTest.php index 6e0e2cc..ca57704 100644 --- a/tests/unit/GettextExtractor/Filters/LatteFilterTest.php +++ b/tests/unit/GettextExtractor/Filters/LatteFilterTest.php @@ -1,15 +1,48 @@ object = new GE\Filters\LatteFilter(); - $this->file = __DIR__ . '/../../data/latte/default.latte'; + } + + public function testExtract(): void { + $messages = $this->object->extract(__DIR__ . '/../../data/latte/default.latte'); + + $this->assertIsArray($messages); + + $this->assertContains(array( + GE\Extractor::LINE => 2, + GE\Extractor::SINGULAR => 'A message!' + ), $messages); + + $this->assertContains(array( + GE\Extractor::LINE => 3, + GE\Extractor::SINGULAR => 'Another message!', + GE\Extractor::CONTEXT => 'context' + ), $messages); + + $this->assertContains(array( + GE\Extractor::LINE => 4, + GE\Extractor::SINGULAR => 'I see %d little indian!', + GE\Extractor::PLURAL => 'I see %d little indians!' + ), $messages); + + $this->assertContains(array( + GE\Extractor::LINE => 5, + GE\Extractor::SINGULAR => 'I see %d little indian!', + GE\Extractor::PLURAL => 'I see %d little indians!', + GE\Extractor::CONTEXT => 'context' + ), $messages); } public function testNoValidMessages(): void { diff --git a/tests/unit/GettextExtractor/Filters/PHPFilterTest.php b/tests/unit/GettextExtractor/Filters/PHPFilterTest.php index bbc2e45..2fda3b2 100644 --- a/tests/unit/GettextExtractor/Filters/PHPFilterTest.php +++ b/tests/unit/GettextExtractor/Filters/PHPFilterTest.php @@ -1,18 +1,49 @@ object = new GE\Filters\PHPFilter(); $this->object->addFunction('addRule', 2); - $this->file = __DIR__ . '/../../data/php/default.php'; + } + + public function testExtract(): void { + $messages = $this->object->extract(__DIR__ . '/../../data/php/default.php'); + + $this->assertIsArray($messages); + + $this->assertContains(array( + GE\Extractor::LINE => 2, + GE\Extractor::SINGULAR => 'A message!' + ), $messages); + + $this->assertContains(array( + GE\Extractor::LINE => 3, + GE\Extractor::SINGULAR => 'Another message!', + GE\Extractor::CONTEXT => 'context' + ), $messages); + + $this->assertContains(array( + GE\Extractor::LINE => 4, + GE\Extractor::SINGULAR => 'I see %d little indian!', + GE\Extractor::PLURAL => 'I see %d little indians!' + ), $messages); + + $this->assertContains(array( + GE\Extractor::LINE => 5, + GE\Extractor::SINGULAR => 'I see %d little indian!', + GE\Extractor::PLURAL => 'I see %d little indians!', + GE\Extractor::CONTEXT => 'context' + ), $messages); } public function testNoValidMessages(): void {