1414use PHPStan \Type \FloatType ;
1515use PHPStan \Type \IntegerType ;
1616use PHPStan \Type \NullType ;
17+ use PHPStan \Type \Php \PrintfFormatParser ;
1718use PHPStan \Type \StringAlwaysAcceptingObjectWithToStringType ;
1819use PHPStan \Type \Type ;
1920use PHPStan \Type \UnionType ;
2021use PHPStan \Type \VerbosityLevel ;
2122use function array_key_exists ;
2223use function count ;
24+ use function in_array ;
2325use function sprintf ;
2426
2527/**
@@ -40,7 +42,7 @@ final class PrintfParameterTypeRule implements Rule
4042 ];
4143
4244 public function __construct (
43- private PrintfHelper $ printfHelper ,
45+ private PrintfFormatParser $ printfFormatParser ,
4446 private ReflectionProvider $ reflectionProvider ,
4547 private RuleLevelHelper $ ruleLevelHelper ,
4648 private bool $ checkStrictPrintfPlaceholderTypes ,
@@ -92,7 +94,7 @@ public function processNode(Node $node, Scope $scope): array
9294
9395 $ formatString = $ formatArgTypeStrings [0 ];
9496 $ format = $ formatString ->getValue ();
95- $ placeholderMap = $ this ->printfHelper -> getPrintfPlaceholders ($ format );
97+ $ placeholderMap = $ this ->getPlaceholders ($ format );
9698 if ($ placeholderMap === null ) {
9799 // Already reported by PrintfParametersRule.
98100 return [];
@@ -171,4 +173,60 @@ public function processNode(Node $node, Scope $scope): array
171173 return $ errors ;
172174 }
173175
176+ /**
177+ * @return array<int, non-empty-list<PrintfPlaceholder>>|null parameter index => placeholders
178+ */
179+ private function getPlaceholders (string $ format ): ?array
180+ {
181+ $ uses = $ this ->printfFormatParser ->parse ($ format );
182+ if ($ uses === null ) {
183+ return null ;
184+ }
185+
186+ $ placeholdersWithStar = [];
187+ foreach ($ uses as $ use ) {
188+ if ($ use ['kind ' ] === 'value ' ) {
189+ continue ;
190+ }
191+
192+ $ placeholdersWithStar [$ use ['number ' ]] = true ;
193+ }
194+
195+ $ placeholders = [];
196+ foreach ($ uses as $ use ) {
197+ $ label = sprintf ('"%s" ' , $ use ['placeholder ' ]);
198+ if ($ use ['kind ' ] !== 'value ' ) {
199+ $ label .= sprintf (' (%s) ' , $ use ['kind ' ]);
200+ $ acceptingType = 'strict-int ' ;
201+ } else {
202+ if (isset ($ placeholdersWithStar [$ use ['number ' ]])) {
203+ $ label .= ' (value) ' ;
204+ }
205+ $ acceptingType = $ this ->getAcceptingTypeBySpecifier ($ use ['specifier ' ]);
206+ }
207+
208+ $ placeholders [$ use ['index ' ]][] = new PrintfPlaceholder ($ label , $ use ['number ' ], $ acceptingType );
209+ }
210+
211+ return $ placeholders ;
212+ }
213+
214+ /** @phpstan-return 'string'|'int'|'float'|'mixed' */
215+ private function getAcceptingTypeBySpecifier (string $ specifier ): string
216+ {
217+ if ($ specifier === 's ' ) {
218+ return 'string ' ;
219+ }
220+
221+ if (in_array ($ specifier , ['d ' , 'u ' , 'c ' , 'o ' , 'x ' , 'X ' , 'b ' ], true )) {
222+ return 'int ' ;
223+ }
224+
225+ if (in_array ($ specifier , ['e ' , 'E ' , 'f ' , 'F ' , 'g ' , 'G ' , 'h ' , 'H ' ], true )) {
226+ return 'float ' ;
227+ }
228+
229+ return 'mixed ' ;
230+ }
231+
174232}
0 commit comments