Skip to content

Commit a52a6ad

Browse files
swissspidyCopilot
andauthored
Add initial PHPStan config (#486)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent af46b69 commit a52a6ad

24 files changed

Lines changed: 747 additions & 306 deletions

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"test": [
7272
"@lint",
7373
"@phpcs",
74+
"@phpstan",
7475
"@phpunit",
7576
"@behat"
7677
]

phpstan.neon.dist

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
parameters:
2+
level: 9
3+
paths:
4+
- src
5+
- i18n-command.php
6+
scanDirectories:
7+
- vendor/wp-cli/wp-cli/php
8+
scanFiles:
9+
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
10+
- tests/phpstan/scan-files.php
11+
12+
treatPhpDocTypesAsCertain: false
13+
14+
stubFiles:
15+
- stubs/Gettext.stub

src/AuditCommand.php

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
use Gettext\Translations;
77
use Gettext\Utils\ParsedComment;
88
use WP_CLI;
9+
use WP_CLI\Path;
910
use WP_CLI\Utils;
1011

12+
1113
/**
1214
* Audit strings in a WordPress project.
1315
*
@@ -88,33 +90,46 @@ class AuditCommand extends MakePotCommand {
8890
* # Audit a plugin with GitHub Actions annotations format.
8991
* $ wp i18n audit wp-content/plugins/hello-world --format=github-actions
9092
*
91-
* @when before_wp_load
92-
*
93+
* @param array<string> $args Positional arguments.
94+
* @param array<mixed> $assoc_args Associative arguments.
95+
* @return void
9396
* @throws WP_CLI\ExitException
9497
*/
9598
public function __invoke( $args, $assoc_args ) {
96-
$this->source = realpath( $args[0] );
97-
if ( ! $this->source || ! is_dir( $this->source ) ) {
99+
$source = realpath( $args[0] );
100+
if ( ! $source || ! is_dir( $source ) ) {
98101
WP_CLI::error( 'Not a valid source directory.' );
99102
}
100103

101-
$this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) );
102-
$this->domain = Utils\get_flag_value( $assoc_args, 'domain', null );
103-
$this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js );
104-
$this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php );
105-
$this->skip_blade = Utils\get_flag_value( $assoc_args, 'skip-blade', $this->skip_blade );
106-
$this->skip_block_json = Utils\get_flag_value( $assoc_args, 'skip-block-json', $this->skip_block_json );
107-
$this->skip_theme_json = Utils\get_flag_value( $assoc_args, 'skip-theme-json', $this->skip_theme_json );
108-
$this->format = Utils\get_flag_value( $assoc_args, 'format', $this->format );
109-
$ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false );
110-
111-
$include = Utils\get_flag_value( $assoc_args, 'include', [] );
112-
if ( ! empty( $include ) ) {
104+
$this->source = $source;
105+
106+
/** @var array<string, bool|string> $assoc_args_simple */
107+
$assoc_args_simple = $assoc_args;
108+
109+
$slug = Utils\get_flag_value( $assoc_args_simple, 'slug', Path::basename( $this->source ) );
110+
$this->slug = is_string( $slug ) ? $slug : Path::basename( $this->source );
111+
112+
$domain = Utils\get_flag_value( $assoc_args_simple, 'domain', null );
113+
$this->domain = is_string( $domain ) ? $domain : null;
114+
115+
$this->skip_js = (bool) Utils\get_flag_value( $assoc_args_simple, 'skip-js', $this->skip_js );
116+
$this->skip_php = (bool) Utils\get_flag_value( $assoc_args_simple, 'skip-php', $this->skip_php );
117+
$this->skip_blade = (bool) Utils\get_flag_value( $assoc_args_simple, 'skip-blade', $this->skip_blade );
118+
$this->skip_block_json = (bool) Utils\get_flag_value( $assoc_args_simple, 'skip-block-json', $this->skip_block_json );
119+
$this->skip_theme_json = (bool) Utils\get_flag_value( $assoc_args_simple, 'skip-theme-json', $this->skip_theme_json );
120+
121+
$format = Utils\get_flag_value( $assoc_args_simple, 'format', $this->format );
122+
$this->format = is_string( $format ) ? $format : 'plaintext';
123+
124+
$ignore_domain = (bool) Utils\get_flag_value( $assoc_args_simple, 'ignore-domain', false );
125+
126+
$include = Utils\get_flag_value( $assoc_args_simple, 'include', null );
127+
if ( is_string( $include ) && '' !== $include ) {
113128
$this->include = array_map( 'trim', explode( ',', $include ) );
114129
}
115130

116-
$exclude = Utils\get_flag_value( $assoc_args, 'exclude', [] );
117-
if ( ! empty( $exclude ) ) {
131+
$exclude = Utils\get_flag_value( $assoc_args_simple, 'exclude', null );
132+
if ( is_string( $exclude ) && '' !== $exclude ) {
118133
$this->exclude = array_map( 'trim', explode( ',', $exclude ) );
119134
}
120135

@@ -128,7 +143,7 @@ public function __invoke( $args, $assoc_args ) {
128143
if ( null === $this->domain ) {
129144
$this->domain = $this->slug;
130145

131-
if ( ! empty( $this->main_file_data['Text Domain']['value'] ) ) {
146+
if ( ! empty( $this->main_file_data['Text Domain']['value'] ) && is_string( $this->main_file_data['Text Domain']['value'] ) ) {
132147
$this->domain = $this->main_file_data['Text Domain']['value'];
133148
}
134149
}
@@ -166,24 +181,28 @@ public function __invoke( $args, $assoc_args ) {
166181
*
167182
* Overrides parent method to suppress log messages when using non-plaintext formats.
168183
*
169-
* @return array
184+
* @return array<string, array<string, mixed>>
170185
*/
171186
protected function get_main_file_data() {
172187
$files = new \IteratorIterator( new \DirectoryIterator( $this->source ) );
173188

174189
/** @var \DirectoryIterator $file */
175190
foreach ( $files as $file ) {
191+
$real_path = $file->getRealPath();
192+
if ( false === $real_path ) {
193+
continue;
194+
}
176195
$stylesheet_path = null;
177196
$project_path = null;
178197

179198
// wp-content/themes/my-theme/style.css
180199
if ( $file->isFile() && 'style' === $file->getBasename( '.css' ) && $file->isReadable() ) {
181-
$stylesheet_path = $file->getRealPath();
182-
$project_path = $file->getRealPath();
183-
} elseif ( $file->isDir() && ! $file->isDot() && is_readable( $file->getRealPath() . '/style.css' ) ) {
200+
$stylesheet_path = $real_path;
201+
$project_path = $real_path;
202+
} elseif ( $file->isDir() && ! $file->isDot() && is_readable( $real_path . '/style.css' ) ) {
184203
// wp-content/themes/my-themes/theme-a/style.css
185-
$stylesheet_path = $file->getRealPath() . '/style.css';
186-
$project_path = $file->getRealPath();
204+
$stylesheet_path = $real_path . '/style.css';
205+
$project_path = $real_path;
187206
}
188207

189208
if ( $stylesheet_path ) {
@@ -202,8 +221,10 @@ protected function get_main_file_data() {
202221
}
203222
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $stylesheet_path ), 'audit' );
204223

205-
$this->project_type = 'theme';
206-
$this->main_file_path = $project_path;
224+
$this->project_type = 'theme';
225+
if ( is_string( $project_path ) ) {
226+
$this->main_file_path = $project_path;
227+
}
207228

208229
return $theme_data;
209230
}
@@ -212,7 +233,7 @@ protected function get_main_file_data() {
212233
// wp-content/plugins/my-plugin/my-plugin.php
213234
if ( $file->isFile() && $file->isReadable() && 'php' === $file->getExtension() ) {
214235
$plugin_data = FileDataExtractor::get_file_data(
215-
$file->getRealPath(),
236+
$real_path,
216237
array_combine(
217238
$this->get_file_headers( 'plugin' ),
218239
$this->get_file_headers( 'plugin' )
@@ -224,10 +245,10 @@ protected function get_main_file_data() {
224245
if ( 'plaintext' === $this->format ) {
225246
WP_CLI::log( 'Plugin file detected.' );
226247
}
227-
WP_CLI::debug( sprintf( 'Plugin file: %s', $file->getRealPath() ), 'audit' );
248+
WP_CLI::debug( sprintf( 'Plugin file: %s', $real_path ), 'audit' );
228249

229250
$this->project_type = 'plugin';
230-
$this->main_file_path = $file->getRealPath();
251+
$this->main_file_path = $real_path;
231252

232253
return $plugin_data;
233254
}
@@ -378,7 +399,7 @@ protected function get_comment_text( $comment ) {
378399
* Goes through all extracted strings to find possible mistakes.
379400
*
380401
* @param Translations $translations Translations object.
381-
* @return array Array of issues found.
402+
* @return array<int, array<string, mixed>> Array of issues found.
382403
*/
383404
protected function collect_audit_issues( $translations ) {
384405
$issues = [];
@@ -420,8 +441,6 @@ protected function collect_audit_issues( $translations ) {
420441
$comments = array_filter(
421442
$comments,
422443
function ( $comment ) {
423-
/** @var ParsedComment|string $comment */
424-
/** @var string $file_header */
425444
foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) {
426445
if ( 0 === strpos( $this->get_comment_text( $comment ), $file_header ) ) {
427446
return false;
@@ -453,7 +472,8 @@ function ( $comment ) {
453472
}
454473
}
455474

456-
$non_placeholder_content = trim( preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $translation->getOriginal() ) );
475+
$replaced = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $translation->getOriginal() );
476+
$non_placeholder_content = trim( is_string( $replaced ) ? $replaced : '' );
457477

458478
// Check 3: Flag empty strings without any translatable content.
459479
if ( '' === $non_placeholder_content ) {
@@ -517,7 +537,8 @@ function ( $comment ) {
517537
/**
518538
* Outputs audit results in the specified format.
519539
*
520-
* @param array $issues Array of issues found.
540+
* @param array<int, array<string, mixed>> $issues Array of issues found.
541+
* @return void
521542
*/
522543
protected function output_results( $issues ) {
523544
if ( empty( $issues ) ) {
@@ -526,14 +547,15 @@ protected function output_results( $issues ) {
526547

527548
switch ( $this->format ) {
528549
case 'json':
529-
WP_CLI::line( json_encode( $issues, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
550+
$json = json_encode( $issues, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
551+
WP_CLI::line( false !== $json ? $json : '[]' );
530552
break;
531553

532554
case 'github-actions':
533555
foreach ( $issues as $issue ) {
534-
$file = $issue['file'];
535-
$line = $issue['line'] ?? 1;
536-
$message = $issue['message'];
556+
$file = isset( $issue['file'] ) && is_scalar( $issue['file'] ) ? (string) $issue['file'] : '';
557+
$line = isset( $issue['line'] ) && is_scalar( $issue['line'] ) ? (int) $issue['line'] : 1;
558+
$message = isset( $issue['message'] ) && is_scalar( $issue['message'] ) ? (string) $issue['message'] : '';
537559

538560
WP_CLI::line( sprintf( '::warning file=%s,line=%d::%s', $file, $line, $message ) );
539561
}
@@ -542,9 +564,9 @@ protected function output_results( $issues ) {
542564
case 'plaintext':
543565
default:
544566
foreach ( $issues as $issue ) {
545-
$file = $issue['file'];
546-
$line = $issue['line'] ?? null;
547-
$message = $issue['message'];
567+
$file = isset( $issue['file'] ) && is_scalar( $issue['file'] ) ? (string) $issue['file'] : '';
568+
$line = isset( $issue['line'] ) && is_scalar( $issue['line'] ) ? (int) $issue['line'] : null;
569+
$message = isset( $issue['message'] ) && is_scalar( $issue['message'] ) ? (string) $issue['message'] : '';
548570
$location = $line ? "$file:$line" : $file;
549571

550572
WP_CLI::warning( sprintf( '%s: %s', $location, $message ) );

src/BladeCodeExtractor.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
final class BladeCodeExtractor extends BladeGettextExtractor {
1010
use IterableCodeExtractor;
1111

12+
/**
13+
* @var array<mixed>
14+
*/
1215
public static $options = [
1316
'extractComments' => [ 'translators', 'Translators' ],
1417
'constants' => [],
@@ -42,21 +45,30 @@ final class BladeCodeExtractor extends BladeGettextExtractor {
4245
],
4346
];
4447

48+
/**
49+
* @var string
50+
*/
4551
protected static $functionsScannerClass = 'WP_CLI\I18n\PhpFunctionsScanner';
4652

4753
/**
4854
* {@inheritdoc}
55+
*
56+
* @param string $text The text to extract strings from.
57+
* @param Translations $translations Translations instance.
58+
* @param array<mixed> $options Extraction options.
59+
* @return void
4960
*/
5061
public static function fromString( $text, Translations $translations, array $options = [] ) {
51-
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
62+
$file = isset( $options['file'] ) && is_scalar( $options['file'] ) ? (string) $options['file'] : '';
63+
WP_CLI::debug( "Parsing file {$file}", 'make-pot' );
5264

5365
try {
5466
self::fromStringMultiple( $text, [ $translations ], $options );
5567
} catch ( Exception $exception ) {
5668
WP_CLI::debug(
5769
sprintf(
5870
'Could not parse file %1$s: %2$s',
59-
$options['file'],
71+
$file,
6072
$exception->getMessage()
6173
),
6274
'make-pot'

src/BladeGettextExtractor.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,26 @@ class BladeGettextExtractor extends \Gettext\Extractors\PhpCode {
1616
/**
1717
* Prepares a Blade compiler/engine and returns it.
1818
*
19+
* @param array<string, mixed> $options Options array.
1920
* @return BladeOne
2021
*/
21-
protected static function getBladeCompiler() {
22-
$cache_path = empty( $options['cachePath'] ) ? sys_get_temp_dir() : $options['cachePath'];
23-
$blade_compiler = new BladeOne( null, $cache_path );
22+
protected static function getBladeCompiler( array $options = [] ) {
2423

25-
if ( method_exists( $blade_compiler, 'withoutComponentTags' ) ) {
26-
$blade_compiler->withoutComponentTags();
27-
}
24+
$cache_path = ! empty( $options['cachePath'] ) && is_string( $options['cachePath'] ) ? $options['cachePath'] : sys_get_temp_dir();
25+
$blade_compiler = new BladeOne( null, $cache_path );
2826

2927
return $blade_compiler;
3028
}
3129

3230
/**
3331
* Compiles the Blade template string into a PHP string in one step.
3432
*
35-
* @param string $text Blade string to be compiled to a PHP string
33+
* @param string $text Blade string to be compiled to a PHP string
34+
* @param array<string, mixed> $options Options array.
3635
* @return string
3736
*/
38-
protected static function compileBladeToPhp( $text ) {
39-
return static::getBladeCompiler()->compileString( $text );
37+
protected static function compileBladeToPhp( $text, array $options = [] ) {
38+
return static::getBladeCompiler( $options )->compileString( $text );
4039
}
4140

4241
/**
@@ -76,11 +75,17 @@ protected static function extractComponentPropExpressions( $text ) {
7675
/**
7776
* {@inheritdoc}
7877
*
78+
* @param string $text The text to extract strings from.
79+
* @param array<\Gettext\Translations> $translations Translations instances.
80+
* @param array<mixed> $options Options.
81+
* @return void
82+
*
7983
* Note: In the parent PhpCode class fromString() uses fromStringMultiple() (overridden here)
8084
*/
8185
public static function fromStringMultiple( $text, array $translations, array $options = [] ) {
82-
$php_string = static::compileBladeToPhp( $text );
86+
$php_string = static::compileBladeToPhp( $text, $options );
87+
8388
$php_string .= static::extractComponentPropExpressions( $text );
84-
return parent::fromStringMultiple( $php_string, $translations, $options );
89+
parent::fromStringMultiple( $php_string, $translations, $options );
8590
}
8691
}

src/BlockExtractor.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ final class BlockExtractor extends JsonSchemaExtractor {
1010
* @inheritdoc
1111
*/
1212
public static function fromString( $text, Translations $translations, array $options = [] ) {
13-
$file = $options['file'];
13+
$file = isset( $options['file'] ) && is_scalar( $options['file'] ) ? (string) $options['file'] : '';
1414
WP_CLI::debug( "Parsing file $file", 'make-pot' );
1515

1616
$json = json_decode( $text, true );
@@ -28,6 +28,10 @@ public static function fromString( $text, Translations $translations, array $opt
2828
return;
2929
}
3030

31+
if ( ! is_array( $json ) ) {
32+
return;
33+
}
34+
3135
$domain = isset( $json['textdomain'] ) ? $json['textdomain'] : null;
3236

3337
// Always allow missing domain or when --ignore-domain is used, but skip if domains don't match.

0 commit comments

Comments
 (0)