diff --git a/phpstan.neon b/phpstan.neon index c127e42..689aacd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,13 +7,3 @@ parameters: - tests/bootstrap.php paths: - src - ignoreErrors: - - - message: "#^Parameter \\#1 \\$var_array of function extract is passed by reference, so it expects variables only\\.$#" - count: 1 - path: src/Command/I18nExtractCommand.php - - - - message: "#^Strict comparison using \\!\\=\\= between null and null will always evaluate to false\\.$#" - count: 2 - path: src/Command/I18nExtractCommand.php diff --git a/src/Command/I18nExtractCommand.php b/src/Command/I18nExtractCommand.php index 302ac20..10e1b12 100644 --- a/src/Command/I18nExtractCommand.php +++ b/src/Command/I18nExtractCommand.php @@ -9,7 +9,6 @@ use Cake\Core\App; use Cake\Core\Configure; use Cake\Core\Plugin; -use Cake\Filesystem\Filesystem; use Cake\Utility\Hash; use Cake\Utility\Inflector; @@ -335,294 +334,4 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar return $parser; } - - /** - * Extract tokens out of all files to be processed - * - * @param \Cake\Console\Arguments $args The io instance - * @param \Cake\Console\ConsoleIo $io The io instance - * @return void - */ - protected function _extractTokens(Arguments $args, ConsoleIo $io): void - { - /** @var \Cake\Shell\Helper\ProgressHelper $progress */ - $progress = $io->helper('progress'); - $progress->init(['total' => count($this->_files)]); - $isVerbose = $args->getOption('verbose'); - - $functions = [ - '__' => ['singular'], - '__n' => ['singular', 'plural'], - '__d' => ['domain', 'singular'], - '__dn' => ['domain', 'singular', 'plural'], - '__x' => ['context', 'singular'], - '__xn' => ['context', 'singular', 'plural'], - '__dx' => ['domain', 'context', 'singular'], - '__dxn' => ['domain', 'context', 'singular', 'plural'], - ]; - $pattern = '/(' . implode('|', array_keys($functions)) . ')\s*\(/'; - - foreach ($this->_files as $file) { - $this->_file = $file; - if ($isVerbose) { - $io->verbose(sprintf('Processing %s...', $file)); - } - - $code = file_get_contents($file); - - if (preg_match($pattern, $code) === 1) { - $allTokens = token_get_all($code); - - $this->_tokens = []; - foreach ($allTokens as $token) { - if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) { - $this->_tokens[] = $token; - } - } - unset($allTokens); - - foreach ($functions as $functionName => $map) { - $this->_parse($io, $functionName, $map); - } - } - - if (!$isVerbose) { - $progress->increment(1); - $progress->draw(); - } - } - } - - /** - * Parse tokens - * - * @param \Cake\Console\ConsoleIo $io The io instance - * @param string $functionName Function name that indicates translatable string (e.g: '__') - * @param array $map Array containing what variables it will find (e.g: domain, singular, plural) - * @return void - */ - protected function _parse(ConsoleIo $io, string $functionName, array $map): void - { - $count = 0; - $tokenCount = count($this->_tokens); - - while ($tokenCount - $count > 1) { - $countToken = $this->_tokens[$count]; - $firstParenthesis = $this->_tokens[$count + 1]; - if (!is_array($countToken)) { - $count++; - continue; - } - - [$type, $string, $line] = $countToken; - if (($type === T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) { - $position = $count; - $depth = 0; - - while (!$depth) { - if ($this->_tokens[$position] === '(') { - $depth++; - } elseif ($this->_tokens[$position] === ')') { - $depth--; - } - $position++; - } - - $mapCount = count($map); - $strings = $this->_getStrings($position, $mapCount); - - if ($mapCount === count($strings)) { - $singular = ''; - $plural = $context = null; - extract(array_combine($map, $strings)); - $domain = $domain ?? 'default'; - $details = [ - 'file' => $this->_file, - 'line' => $line, - ]; - if ($this->_relativePaths) { - $details['file'] = '.' . str_replace(ROOT, '', $details['file']); - } - if ($plural !== null) { - $details['msgid_plural'] = $plural; - } - if ($context !== null) { - $details['msgctxt'] = $context; - } - $this->_addTranslation($domain, $singular, $details); - } else { - $this->_markerError($io, $this->_file, $line, $functionName, $count); - } - } - $count++; - } - } - - /** - * Get the strings from the position forward - * - * @param int $position Actual position on tokens array - * @param int $target Number of strings to extract - * @return array Strings extracted - */ - protected function _getStrings(int &$position, int $target): array - { - $strings = []; - $count = count($strings); - while ( - $count < $target - && ($this->_tokens[$position] === ',' - || $this->_tokens[$position][0] === T_CONSTANT_ENCAPSED_STRING - || $this->_tokens[$position][0] === T_LNUMBER - ) - ) { - $count = count($strings); - if ($this->_tokens[$position][0] === T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') { - $string = ''; - while ( - $this->_tokens[$position][0] === T_CONSTANT_ENCAPSED_STRING - || $this->_tokens[$position] === '.' - ) { - if ($this->_tokens[$position][0] === T_CONSTANT_ENCAPSED_STRING) { - $string .= $this->_formatString($this->_tokens[$position][1]); - } - $position++; - } - $strings[] = $string; - } elseif ($this->_tokens[$position][0] === T_CONSTANT_ENCAPSED_STRING) { - $strings[] = $this->_formatString($this->_tokens[$position][1]); - } elseif ($this->_tokens[$position][0] === T_LNUMBER) { - $strings[] = $this->_tokens[$position][1]; - } - $position++; - } - - return $strings; - } - - /** - * Format a string to be added as a translatable string - * - * @param string $string String to format - * @return string Formatted string - */ - protected function _formatString(string $string): string - { - $quote = substr($string, 0, 1); - $string = substr($string, 1, -1); - if ($quote === '"') { - $string = stripcslashes($string); - } else { - $string = strtr($string, ["\\'" => "'", '\\\\' => '\\']); - } - $string = str_replace("\r\n", "\n", $string); - - return addcslashes($string, "\0..\37\\\""); - } - - /** - * Indicate an invalid marker on a processed file - * - * @param \Cake\Console\ConsoleIo $io The io instance. - * @param string $file File where invalid marker resides - * @param int $line Line number - * @param string $marker Marker found - * @param int $count Count - * @return void - */ - protected function _markerError($io, string $file, int $line, string $marker, int $count): void - { - if (strpos($this->_file, CAKE_CORE_INCLUDE_PATH) === false) { - $this->_countMarkerError++; - } - - if (!$this->_markerError) { - return; - } - - $io->err(sprintf("Invalid marker content in %s:%s\n* %s(", $file, $line, $marker)); - $count += 2; - $tokenCount = count($this->_tokens); - $parenthesis = 1; - - while (($tokenCount - $count > 0) && $parenthesis) { - if (is_array($this->_tokens[$count])) { - $io->err($this->_tokens[$count][1], 0); - } else { - $io->err($this->_tokens[$count], 0); - if ($this->_tokens[$count] === '(') { - $parenthesis++; - } - - if ($this->_tokens[$count] === ')') { - $parenthesis--; - } - } - $count++; - } - $io->err("\n"); - } - - /** - * Search files that may contain translatable strings - * - * @return void - */ - protected function _searchFiles(): void - { - $pattern = false; - if (!empty($this->_exclude)) { - $exclude = []; - foreach ($this->_exclude as $e) { - if (DIRECTORY_SEPARATOR !== '\\' && $e[0] !== DIRECTORY_SEPARATOR) { - $e = DIRECTORY_SEPARATOR . $e; - } - $exclude[] = preg_quote($e, '/'); - } - $pattern = '/' . implode('|', $exclude) . '/'; - } - - foreach ($this->_paths as $path) { - $path = realpath($path) . DIRECTORY_SEPARATOR; - /** @psalm-suppress InternalClass */ - $fs = new Filesystem(); - /** @psalm-suppress InternalMethod */ - $files = $fs->findRecursive($path, '/\.php$/'); - $files = array_keys(iterator_to_array($files)); - sort($files); - if (!empty($pattern)) { - $files = preg_grep($pattern, $files, PREG_GREP_INVERT); - $files = array_values($files); - } - $this->_files = array_merge($this->_files, $files); - } - $this->_files = array_unique($this->_files); - } - - /** - * Returns whether this execution is meant to extract string only from directories in folder represented by the - * APP constant, i.e. this task is extracting strings from same application. - * - * @return bool - */ - protected function _isExtractingApp(): bool - { - /** @psalm-suppress UndefinedConstant */ - return $this->_paths === [APP]; - } - - /** - * Checks whether or not a given path is usable for writing. - * - * @param string $path Path to folder - * @return bool true if it exists and is writable, false otherwise - */ - protected function _isPathUsable($path): bool - { - if (!is_dir($path)) { - mkdir($path, 0770, true); - } - - return is_dir($path) && is_writable($path); - } }