diff --git a/composer.json b/composer.json index 9885618..256b6aa 100644 --- a/composer.json +++ b/composer.json @@ -71,6 +71,7 @@ "test": [ "@lint", "@phpcs", + "@phpstan", "@phpunit", "@behat" ] diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..70099aa --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,15 @@ +parameters: + level: 6 + paths: + - src + - i18n-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan/scan-files.php + + treatPhpDocTypesAsCertain: false + + stubFiles: + - stubs/Gettext.stub diff --git a/src/AuditCommand.php b/src/AuditCommand.php index 88e6324..3029561 100644 --- a/src/AuditCommand.php +++ b/src/AuditCommand.php @@ -6,8 +6,10 @@ use Gettext\Translations; use Gettext\Utils\ParsedComment; use WP_CLI; +use WP_CLI\Path; use WP_CLI\Utils; + /** * Audit strings in a WordPress project. * @@ -88,8 +90,9 @@ class AuditCommand extends MakePotCommand { * # Audit a plugin with GitHub Actions annotations format. * $ wp i18n audit wp-content/plugins/hello-world --format=github-actions * - * @when before_wp_load - * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + * @return void * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { @@ -98,7 +101,7 @@ public function __invoke( $args, $assoc_args ) { WP_CLI::error( 'Not a valid source directory.' ); } - $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) ); + $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Path::basename( $this->source ) ); $this->domain = Utils\get_flag_value( $assoc_args, 'domain', null ); $this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js ); $this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php ); @@ -108,12 +111,12 @@ public function __invoke( $args, $assoc_args ) { $this->format = Utils\get_flag_value( $assoc_args, 'format', $this->format ); $ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false ); - $include = Utils\get_flag_value( $assoc_args, 'include', [] ); + $include = Utils\get_flag_value( $assoc_args, 'include', null ); if ( ! empty( $include ) ) { $this->include = array_map( 'trim', explode( ',', $include ) ); } - $exclude = Utils\get_flag_value( $assoc_args, 'exclude', [] ); + $exclude = Utils\get_flag_value( $assoc_args, 'exclude', null ); if ( ! empty( $exclude ) ) { $this->exclude = array_map( 'trim', explode( ',', $exclude ) ); } @@ -166,7 +169,7 @@ public function __invoke( $args, $assoc_args ) { * * Overrides parent method to suppress log messages when using non-plaintext formats. * - * @return array + * @return array> */ protected function get_main_file_data() { $files = new \IteratorIterator( new \DirectoryIterator( $this->source ) ); @@ -378,7 +381,7 @@ protected function get_comment_text( $comment ) { * Goes through all extracted strings to find possible mistakes. * * @param Translations $translations Translations object. - * @return array Array of issues found. + * @return array> Array of issues found. */ protected function collect_audit_issues( $translations ) { $issues = []; @@ -420,8 +423,6 @@ protected function collect_audit_issues( $translations ) { $comments = array_filter( $comments, function ( $comment ) { - /** @var ParsedComment|string $comment */ - /** @var string $file_header */ foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) { if ( 0 === strpos( $this->get_comment_text( $comment ), $file_header ) ) { return false; @@ -517,7 +518,8 @@ function ( $comment ) { /** * Outputs audit results in the specified format. * - * @param array $issues Array of issues found. + * @param array> $issues Array of issues found. + * @return void */ protected function output_results( $issues ) { if ( empty( $issues ) ) { diff --git a/src/BladeCodeExtractor.php b/src/BladeCodeExtractor.php index 482c68c..3ac184c 100644 --- a/src/BladeCodeExtractor.php +++ b/src/BladeCodeExtractor.php @@ -9,6 +9,9 @@ final class BladeCodeExtractor extends BladeGettextExtractor { use IterableCodeExtractor; + /** + * @var array + */ public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], @@ -42,10 +45,18 @@ final class BladeCodeExtractor extends BladeGettextExtractor { ], ]; + /** + * @var string + */ protected static $functionsScannerClass = 'WP_CLI\I18n\PhpFunctionsScanner'; /** * {@inheritdoc} + * + * @param string $text The text to extract strings from. + * @param Translations $translations Translations instance. + * @param array $options Extraction options. + * @return void */ public static function fromString( $text, Translations $translations, array $options = [] ) { WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' ); diff --git a/src/BladeGettextExtractor.php b/src/BladeGettextExtractor.php index 4fdd055..74c57be 100644 --- a/src/BladeGettextExtractor.php +++ b/src/BladeGettextExtractor.php @@ -16,27 +16,26 @@ class BladeGettextExtractor extends \Gettext\Extractors\PhpCode { /** * Prepares a Blade compiler/engine and returns it. * + * @param array $options Options array. * @return BladeOne */ - protected static function getBladeCompiler() { + protected static function getBladeCompiler( array $options = [] ) { + $cache_path = empty( $options['cachePath'] ) ? sys_get_temp_dir() : $options['cachePath']; $blade_compiler = new BladeOne( null, $cache_path ); - if ( method_exists( $blade_compiler, 'withoutComponentTags' ) ) { - $blade_compiler->withoutComponentTags(); - } - return $blade_compiler; } /** * Compiles the Blade template string into a PHP string in one step. * - * @param string $text Blade string to be compiled to a PHP string + * @param string $text Blade string to be compiled to a PHP string + * @param array $options Options array. * @return string */ - protected static function compileBladeToPhp( $text ) { - return static::getBladeCompiler()->compileString( $text ); + protected static function compileBladeToPhp( $text, array $options = [] ) { + return static::getBladeCompiler( $options )->compileString( $text ); } /** @@ -76,11 +75,17 @@ protected static function extractComponentPropExpressions( $text ) { /** * {@inheritdoc} * + * @param string $text The text to extract strings from. + * @param array<\Gettext\Translations> $translations Translations instances. + * @param array $options Options. + * @return void + * * Note: In the parent PhpCode class fromString() uses fromStringMultiple() (overridden here) */ public static function fromStringMultiple( $text, array $translations, array $options = [] ) { - $php_string = static::compileBladeToPhp( $text ); + $php_string = static::compileBladeToPhp( $text, $options ); + $php_string .= static::extractComponentPropExpressions( $text ); - return parent::fromStringMultiple( $php_string, $translations, $options ); + parent::fromStringMultiple( $php_string, $translations, $options ); } } diff --git a/src/FileDataExtractor.php b/src/FileDataExtractor.php index f724a6b..2148947 100644 --- a/src/FileDataExtractor.php +++ b/src/FileDataExtractor.php @@ -15,10 +15,10 @@ class FileDataExtractor { * * @see get_file_data() * - * @param string $file Path to the file. - * @param array $headers List of headers, in the format array('HeaderKey' => 'Header Name'). + * @param string $file Path to the file. + * @param array $headers List of headers, in the format array('HeaderKey' => 'Header Name'). * - * @return array Array of file headers in `HeaderKey => ['value' => Header Value, 'line' => Line Number]` format. + * @return array Array of file headers in `HeaderKey => ['value' => Header Value, 'line' => Line Number]` format. */ public static function get_file_data( $file, $headers ) { // We don't need to write to the file, so just open for reading. @@ -39,10 +39,10 @@ public static function get_file_data( $file, $headers ) { /** * Retrieves metadata from a string. * - * @param string $text String to look for metadata in. - * @param array $headers List of headers. + * @param string $text String to look for metadata in. + * @param array $headers List of headers. * - * @return array Array of file headers in `HeaderKey => ['value' => Header Value, 'line' => Line Number]` format. + * @return array Array of file headers in `HeaderKey => ['value' => Header Value, 'line' => Line Number]` format. */ public static function get_file_data_from_string( $text, $headers ) { foreach ( $headers as $field => $regex ) { diff --git a/src/IterableCodeExtractor.php b/src/IterableCodeExtractor.php index 112264e..e35a898 100644 --- a/src/IterableCodeExtractor.php +++ b/src/IterableCodeExtractor.php @@ -9,37 +9,42 @@ use RecursiveIteratorIterator; use SplFileInfo; use WP_CLI; -use WP_CLI\Utils; +use WP_CLI\Path; + + trait IterableCodeExtractor { + /** + * @var string + */ protected static $dir = ''; /** * Extract the translations from a file. * - * @param array|string $file_or_files A path of a file or files - * @param Translations $translations The translations instance to append the new translations. - * @param array $options { + * @param array|string $file_or_files A path of a file or files + * @param Translations $translations The translations instance to append the new translations. + * @param array $options { * Optional. An array of options passed down to static::fromString() * * @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'. * @type bool $wpExtractPatterns Extract 'Title' and 'Description' headers in pattern files. Default 'false'. - * @type array $restrictFileNames Skip all files which are not included in this array. - * @type array $restrictDirectories Skip all directories which are not included in this array. + * @type array $restrictFileNames Skip all files which are not included in this array. + * @type array $restrictDirectories Skip all directories which are not included in this array. * } * @return null */ public static function fromFile( $file_or_files, Translations $translations, array $options = [] ) { foreach ( static::getFiles( $file_or_files ) as $file ) { if ( ! empty( $options['restrictFileNames'] ) ) { - $basename = Utils\basename( $file ); + $basename = Path::basename( $file ); if ( ! in_array( $basename, $options['restrictFileNames'], true ) ) { continue; } } - $relative_file_path = ltrim( str_replace( static::$dir, '', Utils\normalize_path( $file ) ), '/' ); + $relative_file_path = ltrim( str_replace( static::$dir, '', Path::normalize( $file ) ), '/' ); // Make sure a relative file path is added as a comment. $options['file'] = $relative_file_path; @@ -121,24 +126,26 @@ public static function fromFile( $file_or_files, Translations $translations, arr static::fromString( $text, $translations, $options ); } + + return null; } /** * Extract the translations from a file. * - * @param string $dir Root path to start the recursive traversal in. - * @param Translations $translations The translations instance to append the new translations. - * @param array $options { + * @param string $dir Root path to start the recursive traversal in. + * @param Translations $translations The translations instance to append the new translations. + * @param array $options { * Optional. An array of options passed down to static::fromString() * * @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'. - * @type array $exclude A list of path to exclude. Default []. - * @type array $extensions A list of extensions to process. Default []. + * @type array $exclude A list of path to exclude. Default []. + * @type array $extensions A list of extensions to process. Default []. * } * @return void */ public static function fromDirectory( $dir, Translations $translations, array $options = [] ) { - $dir = Utils\normalize_path( $dir ); + $dir = Path::normalize( $dir ); static::$dir = $dir; @@ -157,8 +164,8 @@ public static function fromDirectory( $dir, Translations $translations, array $o /** * Determines whether a file is valid based on given matchers. * - * @param SplFileInfo $file File or directory. - * @param array $matchers List of files and directories to match. + * @param SplFileInfo $file File or directory. + * @param array $matchers List of files and directories to match. * @return int How strongly the file is matched. */ protected static function calculateMatchScore( SplFileInfo $file, array $matchers = [] ) { @@ -211,8 +218,8 @@ static function ( $component ) { /** * Determines whether or not a directory has children that may be matched. * - * @param SplFileInfo $dir Directory. - * @param array $matchers List of files and directories to match. + * @param SplFileInfo $dir Directory. + * @param array $matchers List of files and directories to match. * @return bool Whether or not there are any matchers for children of this directory. */ protected static function containsMatchingChildren( SplFileInfo $dir, array $matchers = [] ) { @@ -252,12 +259,12 @@ protected static function containsMatchingChildren( SplFileInfo $dir, array $mat /** * Recursively gets all PHP files within a directory. * - * @param string $dir A path of a directory. - * @param array $includes List of files and directories to include. - * @param array $excludes List of files and directories to skip. - * @param array $extensions List of filename extensions to process. + * @param string $dir A path of a directory. + * @param array $includes List of files and directories to include. + * @param array $excludes List of files and directories to skip. + * @param array $extensions List of filename extensions to process. * - * @return array File list. + * @return array File list. */ public static function getFilesFromDirectory( $dir, array $includes = [], array $excludes = [], $extensions = [] ) { $filtered_files = []; @@ -308,7 +315,7 @@ static function ( $file, $key, $iterator ) use ( $includes, $excludes, $extensio continue; } - $filtered_files[] = Utils\normalize_path( $file->getPathname() ); + $filtered_files[] = Path::normalize( $file->getPathname() ); } sort( $filtered_files, SORT_NATURAL | SORT_FLAG_CASE ); @@ -320,8 +327,8 @@ static function ( $file, $key, $iterator ) use ( $includes, $excludes, $extensio * Determines whether the file extension of a file matches any of the given file extensions. * The end/last part of a multi file extension must also match (`js` of `min.js`). * - * @param SplFileInfo $file File or directory. - * @param array $extensions List of file extensions to match. + * @param SplFileInfo $file File or directory. + * @param array $extensions List of file extensions to match. * @return bool Whether the file has a file extension that matches any of the ones in the list. */ protected static function file_has_file_extension( $file, $extensions ) { diff --git a/src/JedGenerator.php b/src/JedGenerator.php index 7665a42..80224ed 100644 --- a/src/JedGenerator.php +++ b/src/JedGenerator.php @@ -14,6 +14,10 @@ class JedGenerator extends Jed { /** * {@parentDoc}. + * + * @param Translations $translations Translations instance. + * @param array $options Options. + * @return string */ public static function toString( Translations $translations, array $options = [] ) { $options += static::$options; @@ -38,15 +42,16 @@ public static function toString( Translations $translations, array $options = [] ], ]; - return json_encode( $data, $options['json'] ); + $result = json_encode( $data, $options['json'] ); + return false === $result ? '' : $result; } /** * Generates an array with all translations. * - * @param Translations $translations + * @param Translations $translations Translations instance. * - * @return array + * @return array> */ public static function buildMessages( Translations $translations ) { $plural_forms = $translations->getPluralForms(); diff --git a/src/JsCodeExtractor.php b/src/JsCodeExtractor.php index 4c03ee5..3460a30 100644 --- a/src/JsCodeExtractor.php +++ b/src/JsCodeExtractor.php @@ -11,6 +11,9 @@ final class JsCodeExtractor extends JsCode { use IterableCodeExtractor; + /** + * @var array + */ public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], @@ -22,10 +25,18 @@ final class JsCodeExtractor extends JsCode { ], ]; + /** + * @var string + */ protected static $functionsScannerClass = 'WP_CLI\I18n\JsFunctionsScanner'; /** - * @inheritdoc + * {@inheritdoc} + * + * @param string $text The text to extract strings from. + * @param Translations $translations Translations instance. + * @param array $options Extraction options. + * @return void */ public static function fromString( $text, Translations $translations, array $options = [] ) { WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' ); @@ -56,7 +67,12 @@ public static function fromString( $text, Translations $translations, array $opt } /** - * @inheritDoc + * {@inheritdoc} + * + * @param string $text The text to extract strings from. + * @param array<\Gettext\Translations> $translations Translations instances. + * @param array $options Extraction options. + * @return void */ public static function fromStringMultiple( $text, array $translations, array $options = [] ) { $options += self::$options; @@ -64,6 +80,9 @@ public static function fromStringMultiple( $text, array $translations, array $op /** @var JsFunctionsScanner $functions */ $functions = new self::$functionsScannerClass( $text ); $functions->enableCommentsExtraction( $options['extractComments'] ); - $functions->saveGettextFunctions( $translations, $options ); + + if ( ! empty( $translations ) ) { + $functions->saveGettextFunctions( $translations[0], $options ); + } } } diff --git a/src/JsFunctionsScanner.php b/src/JsFunctionsScanner.php index 97fa25d..0ca6890 100644 --- a/src/JsFunctionsScanner.php +++ b/src/JsFunctionsScanner.php @@ -13,7 +13,7 @@ final class JsFunctionsScanner extends GettextJsFunctionsScanner { /** * If not false, comments will be extracted. * - * @var string|false|array + * @var string|false|array */ private $extract_comments = false; @@ -29,7 +29,8 @@ final class JsFunctionsScanner extends GettextJsFunctionsScanner { /** * Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted). * - * @param mixed $tag + * @param string|array $tag Tag to extract. + * @return void */ public function enableCommentsExtraction( $tag = '' ) { $this->extract_comments = $tag; @@ -37,6 +38,8 @@ public function enableCommentsExtraction( $tag = '' ) { /** * Disable comments extraction. + * + * @return void */ public function disableCommentsExtraction() { $this->extract_comments = false; @@ -44,6 +47,10 @@ public function disableCommentsExtraction() { /** * {@inheritdoc} + * + * @param \Gettext\Translations $translations Translations instance. + * @param array $options Options. + * @return void */ public function saveGettextFunctions( $translations, array $options ) { // Ignore multiple translations for now. @@ -75,13 +82,12 @@ function ( $node ) use ( &$translations, $options, &$all_comments ) { $file = $options['file']; $add_reference = ! empty( $options['addReferences'] ); - /** @var Node\Node $node */ foreach ( $node->getLeadingComments() as $comment ) { $all_comments[] = $comment; } /** @var Node\CallExpression $node */ - if ( 'CallExpression' !== $node->getType() ) { + if ( ! $node instanceof Node\CallExpression ) { return; } @@ -91,7 +97,6 @@ function ( $node ) use ( &$translations, $options, &$all_comments ) { return; } - /** @var Node\CallExpression $node */ foreach ( $node->getArguments() as $argument ) { // Support nested function calls. $argument->setLeadingComments( $argument->getLeadingComments() + $node->getLeadingComments() ); @@ -107,7 +112,6 @@ function ( $node ) use ( &$translations, $options, &$all_comments ) { $plural = null; $args = []; - /** @var Node\Node $argument */ foreach ( $node->getArguments() as $argument ) { foreach ( $argument->getLeadingComments() as $comment ) { $all_comments[] = $comment; @@ -121,13 +125,13 @@ function ( $node ) use ( &$translations, $options, &$all_comments ) { continue; } - if ( 'Literal' === $argument->getType() ) { + if ( $argument instanceof Node\Literal ) { /** @var Node\Literal $argument */ $args[] = $argument->getValue(); continue; } - if ( 'TemplateLiteral' === $argument->getType() && 0 === count( $argument->getExpressions() ) ) { + if ( $argument instanceof Node\TemplateLiteral && 0 === count( $argument->getExpressions() ) ) { /** @var Node\TemplateLiteral $argument */ /** @var Node\TemplateElement[] $parts */ @@ -233,7 +237,6 @@ function ( $node ) use ( &$translations, $options, $scanner ) { } $eval_contents = ''; - /** @var Node\Node $argument */ foreach ( $node->getArguments() as $argument ) { if ( 'Literal' === $argument->getType() ) { /** @var Node\Literal $argument */ @@ -264,14 +267,14 @@ function ( $node ) use ( &$translations, $options, $scanner ) { * * @param Node\CallExpression $node The call expression whose callee to resolve. * - * @return array|bool Array containing the name and comments of the identifier if resolved. False if not. + * @return array|bool Array containing the name and comments of the identifier if resolved. False if not. */ private function resolveExpressionCallee( Node\CallExpression $node ) { $callee = $node->getCallee(); // If the callee is a simple identifier it can simply be returned. // For example: __( "translation" ). - if ( 'Identifier' === $callee->getType() ) { + if ( $callee instanceof Node\Identifier ) { return [ 'name' => $callee->getName(), 'comments' => $callee->getLeadingComments(), @@ -281,11 +284,11 @@ private function resolveExpressionCallee( Node\CallExpression $node ) { // If the callee is a member expression resolve it to the property. // For example: wp.i18n.__( "translation" ) or u.__( "translation" ). if ( - 'MemberExpression' === $callee->getType() && - 'Identifier' === $callee->getProperty()->getType() + $callee instanceof Node\MemberExpression && + $callee->getProperty() instanceof Node\Identifier ) { // Make sure to unpack wp.i18n which is a nested MemberExpression. - $comments = 'MemberExpression' === $callee->getObject()->getType() + $comments = $callee->getObject() instanceof Node\MemberExpression ? $callee->getObject()->getObject()->getLeadingComments() : $callee->getObject()->getLeadingComments(); @@ -298,16 +301,16 @@ private function resolveExpressionCallee( Node\CallExpression $node ) { // If the callee is a call expression as created by Webpack resolve it. // For example: Object(u.__)( "translation" ). if ( - 'CallExpression' === $callee->getType() && - 'Identifier' === $callee->getCallee()->getType() && + $callee instanceof Node\CallExpression && + $callee->getCallee() instanceof Node\Identifier && 'Object' === $callee->getCallee()->getName() && [] !== $callee->getArguments() && - 'MemberExpression' === $callee->getArguments()[0]->getType() + $callee->getArguments()[0] instanceof Node\MemberExpression ) { $property = $callee->getArguments()[0]->getProperty(); // Matches minified webpack statements: Object(u.__)( "translation" ). - if ( 'Identifier' === $property->getType() ) { + if ( $property instanceof Node\Identifier ) { return [ 'name' => $property->getName(), 'comments' => $callee->getCallee()->getLeadingComments(), @@ -316,7 +319,7 @@ private function resolveExpressionCallee( Node\CallExpression $node ) { // Matches unminified webpack statements: // Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__["__"])( "translation" ); - if ( 'Literal' === $property->getType() ) { + if ( $property instanceof Node\Literal ) { $name = $property->getValue(); // Matches mangled webpack statement: @@ -336,14 +339,14 @@ private function resolveExpressionCallee( Node\CallExpression $node ) { // If the callee is an indirect function call as created by babel, resolve it. // For example: `(0, u.__)( "translation" )`. if ( - 'ParenthesizedExpression' === $callee->getType() - && 'SequenceExpression' === $callee->getExpression()->getType() + $callee instanceof Node\ParenthesizedExpression + && $callee->getExpression() instanceof Node\SequenceExpression && 2 === count( $callee->getExpression()->getExpressions() ) - && 'Literal' === $callee->getExpression()->getExpressions()[0]->getType() + && $callee->getExpression()->getExpressions()[0] instanceof Node\Literal && [] !== $node->getArguments() ) { // Matches any general indirect function call: `(0, __)( "translation" )`. - if ( 'Identifier' === $callee->getExpression()->getExpressions()[1]->getType() ) { + if ( $callee->getExpression()->getExpressions()[1] instanceof Node\Identifier ) { return [ 'name' => $callee->getExpression()->getExpressions()[1]->getName(), 'comments' => $callee->getLeadingComments(), @@ -351,10 +354,10 @@ private function resolveExpressionCallee( Node\CallExpression $node ) { } // Matches indirect function calls used by babel for module imports: `(0, _i18n.__)( "translation" )`. - if ( 'MemberExpression' === $callee->getExpression()->getExpressions()[1]->getType() ) { + if ( $callee->getExpression()->getExpressions()[1] instanceof Node\MemberExpression ) { $property = $callee->getExpression()->getExpressions()[1]->getProperty(); - if ( 'Identifier' === $property->getType() ) { + if ( $property instanceof Node\Identifier ) { return [ 'name' => $property->getName(), 'comments' => $callee->getLeadingComments(), diff --git a/src/JsonSchemaExtractor.php b/src/JsonSchemaExtractor.php index 63ae0f5..0ed0fa9 100644 --- a/src/JsonSchemaExtractor.php +++ b/src/JsonSchemaExtractor.php @@ -80,7 +80,12 @@ protected static function load_schema( $schema, $fallback ) { } /** - * @inheritdoc + * {@inheritdoc} + * + * @param string $text The text to extract strings from. + * @param Translations $translations Translations instance. + * @param array $options Extraction options. + * @return void */ public static function fromString( $text, Translations $translations, array $options = [] ) { $file = $options['file']; @@ -114,10 +119,10 @@ public static function fromString( $text, Translations $translations, array $opt /** * Extract strings from a JSON file using its i18n schema. * - * @param Translations $translations The translations instance to append the new translations. - * @param string|null $file JSON file name or null if no reference should be added. - * @param string|string[]|array[]|object $i18n_schema I18n schema for the setting. - * @param string|string[]|array[] $settings Value for the settings. + * @param Translations $translations The translations instance to append the new translations. + * @param string|null $file JSON file name or null if no reference should be added. + * @param mixed $i18n_schema I18n schema for the setting. + * @param mixed $settings Value for the settings. * * @return void */ diff --git a/src/MakeJsonCommand.php b/src/MakeJsonCommand.php index b52c9a2..b5c8bb5 100644 --- a/src/MakeJsonCommand.php +++ b/src/MakeJsonCommand.php @@ -67,6 +67,10 @@ class MakeJsonCommand extends WP_CLI_Command { * # Create JSON files with object mapping * $ wp i18n make-json languages '--use-map={"source/index.js":"build/index.js"}' * + * @param array $args Command arguments. + * @param array $assoc_args Associative arguments. + * @return void + * * @when before_wp_load * * @throws WP_CLI\ExitException @@ -129,8 +133,8 @@ function ( $extension ) { /** * Collect maps from paths, normalize and merge * - * @param string|array|bool $paths_or_maps argument. False to do nothing. - * @return array|null Mapping array. Null if no maps specified. + * @param string|array|bool $paths_or_maps argument. False to do nothing. + * @return array|null Mapping array. Null if no maps specified. */ protected function build_map( $paths_or_maps ) { if ( false === $paths_or_maps ) { @@ -204,12 +208,12 @@ static function ( $value ) { /** * Splits a single PO file into multiple JSON files. * - * @param string $source_file Path to the source file. - * @param string $destination Path to the destination directory. - * @param array|null $map Source to build file mapping. - * @param string $domain Override text domain to use. - * @param array $extensions Additional extensions. - * @return array List of created JSON files. + * @param string $source_file Path to the source file. + * @param string $destination Path to the destination directory. + * @param array|null $map Source to build file mapping. + * @param string $domain Override text domain to use. + * @param array $extensions Additional extensions. + * @return array List of created JSON files. */ protected function make_json( $source_file, $destination, $map, $domain, $extensions ) { /** @var Translations[] $mapping */ @@ -277,9 +281,9 @@ static function ( $reference ) use ( $extensions ) { /** * Takes the references and applies map, if given * - * @param array $references translation references - * @param array|null $map file mapping - * @return array mapped references + * @param array $references translation references + * @param array|null $map file mapping + * @return array mapped references */ protected function reference_map( $references, $map ) { if ( is_null( $map ) ) { @@ -321,11 +325,11 @@ static function ( $value ) { * * Exports translations for each JS file to a separate translation file. * - * @param array $mapping A mapping of files to translation entries. - * @param string $base_file_name Base file name for JSON files. - * @param string $destination Path to the destination directory. + * @param array $mapping A mapping of files to translation entries. + * @param string $base_file_name Base file name for JSON files. + * @param string $destination Path to the destination directory. * - * @return array List of created JSON files. + * @return array List of created JSON files. */ protected function build_json_files( $mapping, $base_file_name, $destination ) { $result = []; diff --git a/src/MakeMoCommand.php b/src/MakeMoCommand.php index 2a0057c..5838e5d 100644 --- a/src/MakeMoCommand.php +++ b/src/MakeMoCommand.php @@ -35,6 +35,10 @@ class MakeMoCommand extends WP_CLI_Command { * # Create a MO file from a single PO file to a specific file destination * $ wp i18n make-mo example-plugin-de_DE.po languages/bar.mo * + * @param array $args Command arguments. + * @param array $assoc_args Associative arguments. + * @return void + * * @when before_wp_load * * @throws WP_CLI\ExitException diff --git a/src/MakePhpCommand.php b/src/MakePhpCommand.php index baeed19..f9dfd33 100644 --- a/src/MakePhpCommand.php +++ b/src/MakePhpCommand.php @@ -41,6 +41,10 @@ class MakePhpCommand extends WP_CLI_Command { * $ wp i18n make-php example-plugin-de_DE.po languages --pretty-print * Success: Created 1 file. * + * @param array $args Command arguments. + * @param array $assoc_args Associative arguments. + * @return void + * * @when before_wp_load * * @throws WP_CLI\ExitException diff --git a/src/MakePotCommand.php b/src/MakePotCommand.php index 3d14245..599c824 100644 --- a/src/MakePotCommand.php +++ b/src/MakePotCommand.php @@ -9,7 +9,9 @@ use Gettext\Utils\ParsedComment; use WP_CLI; use WP_CLI_Command; +use WP_CLI\Path; use WP_CLI\Utils; + use DirectoryIterator; use IteratorIterator; @@ -25,7 +27,7 @@ class MakePotCommand extends WP_CLI_Command { protected $destination; /** - * @var array + * @var array */ protected $merge = []; @@ -40,12 +42,12 @@ class MakePotCommand extends WP_CLI_Command { protected $subtract_and_merge; /** - * @var array + * @var array */ protected $include = []; /** - * @var array + * @var array */ protected $exclude = [ 'node_modules', '.*', 'vendor', 'Gruntfile.js', 'webpack.config.js', '*.min.js', 'test', 'tests' ]; @@ -55,7 +57,7 @@ class MakePotCommand extends WP_CLI_Command { protected $slug; /** - * @var array + * @var array */ protected $main_file_data = []; @@ -100,7 +102,7 @@ class MakePotCommand extends WP_CLI_Command { protected $location = true; /** - * @var array + * @var array */ protected $headers = []; @@ -115,9 +117,9 @@ class MakePotCommand extends WP_CLI_Command { protected $package_name; /** - * @var string + * @var string|null */ - protected $file_comment; + protected $file_comment = null; /** * @var string @@ -282,6 +284,10 @@ class MakePotCommand extends WP_CLI_Command { * # Create a POT file for the WordPress theme in the current directory with custom headers. * $ wp i18n make-pot . languages/my-theme.pot --headers='{"Report-Msgid-Bugs-To":"https://github.com/theme-author/my-theme/","POT-Creation-Date":""}' * + * @param array $args Command arguments. + * @param array $assoc_args Associative arguments. + * @return void + * * @when before_wp_load * * @throws WP_CLI\ExitException @@ -315,25 +321,29 @@ public function __invoke( $args, $assoc_args ) { * * @throws WP_CLI\ExitException * - * @param array $args Command arguments. - * @param array $assoc_args Associative arguments. + * @param array $args Command arguments. + * @param array $assoc_args Associative arguments. + * @return void */ public function handle_arguments( $args, $assoc_args ) { $array_arguments = array( 'headers' ); $assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments ); $this->source = realpath( $args[0] ); - $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) ); + $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Path::basename( $this->source ) ); $this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js ); $this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php ); $this->skip_blade = Utils\get_flag_value( $assoc_args, 'skip-blade', $this->skip_blade ); $this->skip_block_json = Utils\get_flag_value( $assoc_args, 'skip-block-json', $this->skip_block_json ); $this->skip_theme_json = Utils\get_flag_value( $assoc_args, 'skip-theme-json', $this->skip_theme_json ); $this->skip_audit = Utils\get_flag_value( $assoc_args, 'skip-audit', $this->skip_audit ); - $this->headers = Utils\get_flag_value( $assoc_args, 'headers', $this->headers ); - $this->file_comment = Utils\get_flag_value( $assoc_args, 'file-comment' ); - $this->package_name = Utils\get_flag_value( $assoc_args, 'package-name' ); - $this->location = Utils\get_flag_value( $assoc_args, 'location', true ); + $headers = Utils\get_flag_value( $assoc_args, 'headers', null ); + if ( null !== $headers ) { + $this->headers = (array) $headers; // Cast to array if single string given + } + $this->file_comment = Utils\get_flag_value( $assoc_args, 'file-comment' ); + $this->package_name = Utils\get_flag_value( $assoc_args, 'package-name' ); + $this->location = Utils\get_flag_value( $assoc_args, 'location', true ); $ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false ); @@ -462,7 +472,7 @@ protected function unslashit( $text ) { /** * Retrieves the main file data of the plugin or theme. * - * @return array + * @return array */ protected function get_main_file_data() { $files = new IteratorIterator( new DirectoryIterator( $this->source ) ); @@ -546,7 +556,7 @@ protected function get_main_file_data() { * * @param string $type Source type, either theme or plugin. * - * @return array List of file headers. + * @return array List of file headers. */ protected function get_file_headers( $type ) { switch ( $type ) { @@ -629,7 +639,7 @@ protected function extract_strings() { } if ( $this->main_file_path && $this->location ) { - $file_reference = ltrim( str_replace( Utils\normalize_path( "$this->source/" ), '', Utils\normalize_path( $this->main_file_path ) ), '/' ); + $file_reference = ltrim( str_replace( Path::normalize( "$this->source/" ), '', Path::normalize( $this->main_file_path ) ), '/' ); // Add line number if available if ( ! empty( $data['line'] ) ) { $translation->addReference( $file_reference, $data['line'] ); @@ -777,7 +787,8 @@ protected function extract_strings() { * * Goes through all extracted strings to find possible mistakes. * - * @param Translations $translations Translations object. + * @param \Gettext\Translations $translations Translations object. + * @return void */ protected function audit_strings( $translations ) { foreach ( $translations as $translation ) { @@ -809,8 +820,6 @@ protected function audit_strings( $translations ) { $comments = array_filter( $comments, function ( $comment ) { - /** @var ParsedComment|string $comment */ - /** @var string $file_header */ foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) { if ( 0 === strpos( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $file_header ) ) { return null; @@ -829,12 +838,12 @@ function ( $comment ) { function ( $comment ) use ( &$unique_comments ) { /** @var ParsedComment|string $comment */ if ( in_array( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $unique_comments, true ) ) { - return null; + return false; } $unique_comments[] = ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ); - return $comment; + return true; } ); @@ -965,7 +974,8 @@ protected function get_file_comment() { /** * Sets default POT file headers for the project. * - * @param Translations $translations Translations object. + * @param \Gettext\Translations $translations Translations object. + * @return void */ protected function set_default_headers( $translations ) { $name = null; diff --git a/src/MapCodeExtractor.php b/src/MapCodeExtractor.php index 7a38a77..1d0c56d 100644 --- a/src/MapCodeExtractor.php +++ b/src/MapCodeExtractor.php @@ -10,6 +10,9 @@ final class MapCodeExtractor extends JsCode { use IterableCodeExtractor; + /** + * @var array + */ public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], @@ -23,6 +26,11 @@ final class MapCodeExtractor extends JsCode { /** * {@inheritdoc} + * + * @param string $text + * @param Translations $translations + * @param array $options + * @return void */ public static function fromString( $text, Translations $translations, array $options = [] ) { if ( ! array_key_exists( 'file', $options ) || substr( $options['file'], -7 ) !== '.js.map' ) { diff --git a/src/PhpArrayGenerator.php b/src/PhpArrayGenerator.php index ddfc824..c2f46e5 100644 --- a/src/PhpArrayGenerator.php +++ b/src/PhpArrayGenerator.php @@ -12,6 +12,9 @@ * Returns output in the form WordPress uses. */ class PhpArrayGenerator extends PhpArray { + /** + * @var array + */ public static $options = [ 'includeHeaders' => false, 'prettyPrint' => false, @@ -19,21 +22,24 @@ class PhpArrayGenerator extends PhpArray { /** * {@inheritdoc} + * + * @param array $options + * @return string */ public static function toString( Translations $translations, array $options = [] ) { $options = array_merge( static::$options, $options ); $array = static::generate( $translations, $options ); - return ' $options * - * @return array + * @return array */ public static function generate( Translations $translations, array $options = [] ) { $options += static::$options; @@ -48,7 +54,7 @@ public static function generate( Translations $translations, array $options = [] * @param bool $include_headers * @param bool $force_array Unused. * - * @return array + * @return array */ protected static function toArray( Translations $translations, $include_headers, $force_array = false ) { $messages = []; diff --git a/src/PhpCodeExtractor.php b/src/PhpCodeExtractor.php index 6014848..e5aea3c 100644 --- a/src/PhpCodeExtractor.php +++ b/src/PhpCodeExtractor.php @@ -10,6 +10,9 @@ final class PhpCodeExtractor extends PhpCode { use IterableCodeExtractor; + /** + * @var array + */ public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], @@ -43,10 +46,18 @@ final class PhpCodeExtractor extends PhpCode { ], ]; + /** + * @var string + */ protected static $functionsScannerClass = 'WP_CLI\I18n\PhpFunctionsScanner'; /** * {@inheritdoc} + * + * @param string $text + * @param Translations $translations + * @param array $options + * @return void */ public static function fromString( $text, Translations $translations, array $options = [] ) { WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' ); diff --git a/src/PhpFunctionsScanner.php b/src/PhpFunctionsScanner.php index 033ba2d..b770823 100644 --- a/src/PhpFunctionsScanner.php +++ b/src/PhpFunctionsScanner.php @@ -8,6 +8,10 @@ class PhpFunctionsScanner extends GettextPhpFunctionsScanner { /** * {@inheritdoc} + * + * @param \Gettext\Translations $translations Translations instance. + * @param array $options Options. + * @return void */ public function saveGettextFunctions( $translations, array $options ) { // Ignore multiple translations for now. diff --git a/src/PotGenerator.php b/src/PotGenerator.php index ed2f9a5..f3d207d 100644 --- a/src/PotGenerator.php +++ b/src/PotGenerator.php @@ -13,6 +13,9 @@ * adds some comments at the very beginning of the file. */ class PotGenerator extends PoGenerator { + /** + * @var array + */ protected static $comments_before_headers = []; /** @@ -21,6 +24,7 @@ class PotGenerator extends PoGenerator { * Doesn't need to include # in the beginning of lines, these are added automatically. * * @param string $comment File comment. + * @return void */ public static function setCommentBeforeHeaders( $comment ) { $comments = explode( "\n", $comment ); @@ -33,7 +37,11 @@ public static function setCommentBeforeHeaders( $comment ) { } /** - * {@parentDoc}. + * {@inheritdoc} + * + * @param \Gettext\Translations $translations + * @param array $options + * @return string */ public static function toString( Translations $translations, array $options = [] ) { $lines = static::$comments_before_headers; @@ -109,7 +117,7 @@ public static function toString( Translations $translations, array $options = [] * * @return string[] */ - protected static function multilineQuote( $text ) { + protected static function wpMultilineQuote( $text ) { $lines = explode( "\n", $text ); $last = count( $lines ) - 1; @@ -127,12 +135,13 @@ protected static function multilineQuote( $text ) { /** * Add one or more lines depending whether the string is multiline or not. * - * @param array &$lines Array lines should be added to. - * @param string $name Name of the line, e.g. msgstr or msgid_plural. - * @param string $value The line to add. + * @param array &$lines Array lines should be added to. + * @param string $name Name of the line, e.g. msgstr or msgid_plural. + * @param string $value The line to add. + * @return void */ protected static function addLines( array &$lines, $name, $value ) { - $newlines = self::multilineQuote( $value ); + $newlines = self::wpMultilineQuote( $value ); if ( count( $newlines ) === 1 ) { $lines[] = $name . ' ' . $newlines[0]; diff --git a/src/UpdatePoCommand.php b/src/UpdatePoCommand.php index faa8f02..fa77c97 100644 --- a/src/UpdatePoCommand.php +++ b/src/UpdatePoCommand.php @@ -55,6 +55,9 @@ class UpdatePoCommand extends WP_CLI_Command { * * @when before_wp_load * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + * @return void * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { diff --git a/stubs/Gettext.stub b/stubs/Gettext.stub new file mode 100644 index 0000000..f4799b5 --- /dev/null +++ b/stubs/Gettext.stub @@ -0,0 +1,36 @@ + + */ + public static $options; + + /** + * @var string + */ + protected static $functionsScannerClass; + } + + abstract class JsCode { + /** + * @var array + */ + public static $options; + + /** + * @var string + */ + protected static $functionsScannerClass; + } +} + +namespace Gettext\Generators { + class PhpArray { + /** + * @var array + */ + public $options; + } +} diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 0000000..665af52 --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,16 @@ +