55use Nette \Utils \Strings ;
66use PHPStan \DependencyInjection \AutowiredService ;
77use PHPStan \Php \PhpVersion ;
8- use PHPStan \Type \ErrorType ;
9- use PHPStan \Type \IntegerType ;
10- use PHPStan \Type \Type ;
118use function array_filter ;
12- use function array_flip ;
139use function array_keys ;
14- use function array_map ;
15- use function array_reduce ;
1610use function count ;
1711use function in_array ;
1812use function max ;
19- use function sort ;
2013use function sprintf ;
2114use function strlen ;
22- use function usort ;
2315use const PREG_SET_ORDER ;
2416
2517/** @phpstan-type AcceptingTypeString 'strict-int'|'int'|'float'|'string'|'mixed' */
@@ -38,71 +30,18 @@ public function getPrintfPlaceholdersCount(string $format): int
3830 return $ this ->getPlaceholdersCount (self ::PRINTF_SPECIFIER_PATTERN , $ format );
3931 }
4032
41- /** @return array<int, array{string, callable(Type): bool}> position => [type name, matches callback] */
42- public function getPrintfPlaceholderAcceptingTypes (string $ format ): array
33+ /** @phpstan- return array<int, non-empty-list<PrintfPlaceholder>> parameter index => placeholders */
34+ public function getPrintfPlaceholders (string $ format ): array
4335 {
44- $ placeholders = $ this ->parsePlaceholders (self ::PRINTF_SPECIFIER_PATTERN , $ format );
45- $ result = [];
46- // Type on the left can go to the type on the right, but not vice versa.
47- $ typeSequenceMap = array_flip (['int ' , 'float ' , 'string ' , 'mixed ' ]);
48-
49- foreach ($ placeholders as $ position => $ types ) {
50- sort ($ types );
51- $ typeNames = array_map (
52- static fn (string $ t ) => $ t === 'strict-int '
53- ? 'int '
54- : $ t ,
55- $ types ,
56- );
57- $ typeName = array_reduce (
58- $ typeNames ,
59- static fn (string $ carry , string $ type ) => $ typeSequenceMap [$ carry ] < $ typeSequenceMap [$ type ]
60- ? $ carry
61- : $ type ,
62- 'mixed ' ,
63- );
64- $ result [$ position ] = [
65- $ typeName ,
66- static function (Type $ t ) use ($ types ): bool {
67- foreach ($ types as $ acceptingType ) {
68- switch ($ acceptingType ) {
69- case 'strict-int ' :
70- $ subresult = (new IntegerType ())->accepts ($ t , true )->yes ();
71- break ;
72- case 'int ' :
73- $ subresult = ! $ t ->toInteger () instanceof ErrorType;
74- break ;
75- case 'float ' :
76- $ subresult = ! $ t ->toFloat () instanceof ErrorType;
77- break ;
78- // The function signature already limits the parameters to stringable types, so there's
79- // no point in checking string again here.
80- case 'string ' :
81- case 'mixed ' :
82- default :
83- $ subresult = true ;
84- break ;
85- }
86-
87- if (!$ subresult ) {
88- return false ;
89- }
90- }
91-
92- return true ;
93- },
94- ];
95- }
96-
97- return $ result ;
36+ return $ this ->parsePlaceholders (self ::PRINTF_SPECIFIER_PATTERN , $ format );
9837 }
9938
10039 public function getScanfPlaceholdersCount (string $ format ): int
10140 {
10241 return $ this ->getPlaceholdersCount ('(?<specifier>[cdDeEfinosuxX%s]|\[[^\]]+\]) ' , $ format );
10342 }
10443
105- /** @phpstan-return array<int, non-empty-list<AcceptingTypeString >> position => type */
44+ /** @phpstan-return array<int, non-empty-list<PrintfPlaceholder >> parameter index => placeholders */
10645 private function parsePlaceholders (string $ specifiersPattern , string $ format ): array
10746 {
10847 $ addSpecifier = '' ;
@@ -123,38 +62,49 @@ private function parsePlaceholders(string $specifiersPattern, string $format): a
12362 $ placeholders = array_filter ($ matches , static fn (array $ match ): bool => strlen ($ match ['before ' ]) % 2 === 0 );
12463
12564 $ result = [];
126- $ positionalPlaceholders = [];
127- $ idx = 0 ;
65+ $ parsedPlaceholders = [];
66+ $ parameterIdx = 0 ;
67+ $ placeholderNumber = 0 ;
12868
12969 foreach ($ placeholders as $ placeholder ) {
70+ $ placeholderNumber ++;
71+ $ showValueSuffix = false ;
72+
13073 if (isset ($ placeholder ['width ' ]) && $ placeholder ['width ' ] !== '' ) {
131- $ result [$ idx ++] = ['strict-int ' => 1 ];
74+ $ parsedPlaceholders [] = new PrintfPlaceholder (
75+ sprintf ('"%s" (width) ' , $ placeholder [0 ]),
76+ $ parameterIdx ++,
77+ $ placeholderNumber ,
78+ 'strict-int ' ,
79+ );
80+ $ showValueSuffix = true ;
13281 }
13382
13483 if (isset ($ placeholder ['precision ' ]) && $ placeholder ['precision ' ] !== '' ) {
135- $ result [ $ idx ++ ] = [ ' strict-int ' => 1 ];
136- }
137-
138- if ( isset ( $ placeholder [ ' position ' ]) && $ placeholder [ ' position ' ] !== '' ) {
139- // It may reference future position, so we have to process them later.
140- $ positionalPlaceholders [] = $ placeholder ;
141- continue ;
84+ $ parsedPlaceholders [ ] = new PrintfPlaceholder (
85+ sprintf ( ' "%s" (precision) ' , $ placeholder [ 0 ]),
86+ $ parameterIdx ++,
87+ $ placeholderNumber ,
88+ ' strict-int ' ,
89+ ) ;
90+ $ showValueSuffix = true ;
14291 }
14392
144- $ result [$ idx ++][$ this ->getAcceptingTypeBySpecifier ($ placeholder ['specifier ' ] ?? '' )] = 1 ;
93+ $ parsedPlaceholders [] = new PrintfPlaceholder (
94+ sprintf ('"%s" ' , $ placeholder [0 ]) . ($ showValueSuffix ? ' (value) ' : '' ),
95+ isset ($ placeholder ['position ' ]) && $ placeholder ['position ' ] !== ''
96+ ? $ placeholder ['position ' ] - 1
97+ : $ parameterIdx ++,
98+ $ placeholderNumber ,
99+ $ this ->getAcceptingTypeBySpecifier ($ placeholder ['specifier ' ] ?? '' ),
100+ );
145101 }
146102
147- usort (
148- $ positionalPlaceholders ,
149- static fn (array $ a , array $ b ) => (int ) $ a ['position ' ] <=> (int ) $ b ['position ' ],
150- );
151-
152- foreach ($ positionalPlaceholders as $ placeholder ) {
153- $ idx = $ placeholder ['position ' ] - 1 ;
154- $ result [$ idx ][$ this ->getAcceptingTypeBySpecifier ($ placeholder ['specifier ' ] ?? '' )] = 1 ;
103+ foreach ($ parsedPlaceholders as $ placeholder ) {
104+ $ result [$ placeholder ->parameterIndex ][] = $ placeholder ;
155105 }
156106
157- return array_map ( static fn ( array $ a ) => array_keys ( $ a ), $ result) ;
107+ return $ result ;
158108 }
159109
160110 /** @phpstan-return 'string'|'int'|'float'|'mixed' */
0 commit comments