@@ -41,7 +41,11 @@ public static RuntimeScalar sprintf(RuntimeScalar runtimeScalar, RuntimeList lis
41
41
String format = runtimeScalar .toString ();
42
42
43
43
StringBuilder result = new StringBuilder ();
44
- int argIndex = 0 ;
44
+ int argIndex = 0 ; // Sequential argument index
45
+ int maxArgIndexUsed = -1 ; // Track highest argument index used
46
+ boolean hasValidSpecifier = false ; // Track if we have any valid specifiers
47
+ boolean hasPositionalParameter = false ; // Track if any positional parameters are used
48
+ boolean hasInvalidSpecifier = false ; // Track if any invalid specifiers were found
45
49
46
50
// Parse the format string into literals and format specifiers
47
51
SprintfFormatParser .ParseResult parsed = SprintfFormatParser .parse (format );
@@ -60,6 +64,7 @@ public static RuntimeScalar sprintf(RuntimeScalar runtimeScalar, RuntimeList lis
60
64
if (!spec .isValid ) {
61
65
// Just generate the warning, don't add to result
62
66
handleInvalidSpecifier (spec );
67
+ hasInvalidSpecifier = true ;
63
68
}
64
69
continue ; // Skip adding to result and updating argIndex
65
70
}
@@ -69,38 +74,87 @@ public static RuntimeScalar sprintf(RuntimeScalar runtimeScalar, RuntimeList lis
69
74
String formatted = processFormatSpecifier (spec , list , argIndex , formatter );
70
75
result .append (formatted );
71
76
charsWritten += formatted .length ();
77
+ hasInvalidSpecifier = true ;
78
+ // Invalid specifiers don't consume arguments
72
79
continue ;
73
80
}
74
81
82
+ // Check for positional parameters
83
+ if (spec .parameterIndex != null || spec .widthArgIndex != null || spec .precisionArgIndex != null ) {
84
+ hasPositionalParameter = true ;
85
+ }
86
+
87
+ // We have a valid specifier
88
+ hasValidSpecifier = true ;
89
+
75
90
if (spec .conversionChar == 'n' ) {
76
91
// %n doesn't produce output, but does consume an argument
92
+ int targetIndex = spec .parameterIndex != null ? spec .parameterIndex - 1 : argIndex ;
93
+ maxArgIndexUsed = Math .max (maxArgIndexUsed , targetIndex );
77
94
handlePercentN (spec , list , argIndex );
78
95
79
- // Update argument index
96
+ // Update sequential argument index for non-positional width/precision
80
97
if (spec .parameterIndex == null ) {
81
98
argIndex ++; // %n does consume an argument
82
- if (spec .widthFromArg && spec .widthArgIndex == null ) argIndex ++;
83
- if (spec .precisionFromArg && spec .precisionArgIndex == null ) argIndex ++;
99
+ }
100
+ if (spec .widthFromArg && spec .widthArgIndex == null ) {
101
+ argIndex ++;
102
+ }
103
+ if (spec .precisionFromArg && spec .precisionArgIndex == null ) {
104
+ argIndex ++;
84
105
}
85
106
86
107
// Don't add anything to result or charsWritten
87
108
continue ; // Skip to next format element
88
109
} else {
89
- String formatted = processFormatSpecifier (spec , list , argIndex , formatter );
90
- result .append (formatted );
91
- charsWritten += formatted .length ();
110
+ ProcessResult processResult = processFormatSpecifierTracked (spec , list , argIndex , formatter );
111
+ result .append (processResult . formatted );
112
+ charsWritten += processResult . formatted .length ();
92
113
93
- // Update argument index if not using positional parameters
94
- if (spec .parameterIndex == null && spec .conversionChar != '%' ) {
95
- argIndex = updateArgIndex (spec , argIndex );
114
+ // Only update maxArgIndexUsed if this specifier actually consumed arguments
115
+ if (spec .conversionChar != '%' || spec .widthFromArg ) {
116
+ maxArgIndexUsed = Math .max (maxArgIndexUsed , processResult .maxArgIndexUsed );
117
+ }
118
+
119
+ // Update sequential argument index based on what was consumed
120
+ if (spec .parameterIndex == null ) {
121
+ // Non-positional value argument advances the sequential index
122
+ argIndex ++;
123
+ }
124
+ // Width/precision without explicit position also advance sequential index
125
+ if (spec .widthFromArg && spec .widthArgIndex == null ) {
126
+ argIndex ++;
127
+ }
128
+ if (spec .precisionFromArg && spec .precisionArgIndex == null ) {
129
+ argIndex ++;
96
130
}
97
131
}
98
132
}
99
133
}
100
134
135
+ // Only check for redundant arguments if:
136
+ // 1. We had at least one valid specifier that consumed arguments
137
+ // 2. No positional parameters were used
138
+ // 3. No invalid specifiers were found
139
+ // 4. There are unused arguments
140
+ if (hasValidSpecifier && !hasPositionalParameter && !hasInvalidSpecifier &&
141
+ maxArgIndexUsed >= 0 && maxArgIndexUsed + 1 < list .size ()) {
142
+ WarnDie .warn (new RuntimeScalar ("Redundant argument in sprintf" ), new RuntimeScalar ("" ));
143
+ }
144
+
101
145
return new RuntimeScalar (result .toString ());
102
146
}
103
147
148
+ private static class ProcessResult {
149
+ String formatted ;
150
+ int maxArgIndexUsed ;
151
+
152
+ ProcessResult (String formatted , int maxArgIndexUsed ) {
153
+ this .formatted = formatted ;
154
+ this .maxArgIndexUsed = maxArgIndexUsed ;
155
+ }
156
+ }
157
+
104
158
private static void handlePercentN (FormatSpecifier spec ,
105
159
RuntimeList list , int argIndex ) {
106
160
int targetIndex = spec .parameterIndex != null ? spec .parameterIndex - 1 : argIndex ;
@@ -111,20 +165,119 @@ private static void handlePercentN(FormatSpecifier spec,
111
165
}
112
166
}
113
167
168
+ /**
169
+ * Process a single format specifier with tracking.
170
+ */
171
+ private static ProcessResult processFormatSpecifierTracked (
172
+ FormatSpecifier spec ,
173
+ RuntimeList list ,
174
+ int sepArgIndex ,
175
+ SprintfValueFormatter formatter ) {
176
+
177
+ String formatted = processFormatSpecifier (spec , list , sepArgIndex , formatter , -1 );
178
+
179
+ // Calculate max arg index used
180
+ int maxUsed = -1 ;
181
+
182
+ // Handle %% - literal percent sign
183
+ if (spec .conversionChar == '%' ) {
184
+ if (spec .widthFromArg ) {
185
+ // %*% consumes a width argument
186
+ if (spec .widthArgIndex != null ) {
187
+ maxUsed = Math .max (maxUsed , spec .widthArgIndex - 1 );
188
+ } else if (spec .parameterIndex == null ) {
189
+ maxUsed = Math .max (maxUsed , sepArgIndex );
190
+ }
191
+ }
192
+ return new ProcessResult (formatted , maxUsed );
193
+ }
194
+
195
+ // For invalid specifiers, don't track any arguments
196
+ if (!spec .isValid ) {
197
+ return new ProcessResult (formatted , maxUsed );
198
+ }
199
+
200
+ // Special handling for %*v formats
201
+ if (spec .vectorFlag && spec .widthFromArg && spec .raw .matches (".*\\ *v.*" )) {
202
+ // %*v format - first arg is separator
203
+ int currentIndex = sepArgIndex ;
204
+
205
+ // Track separator argument
206
+ maxUsed = Math .max (maxUsed , currentIndex );
207
+ currentIndex ++;
208
+
209
+ if (spec .precisionFromArg ) {
210
+ // %*v*d format - second arg is width
211
+ maxUsed = Math .max (maxUsed , currentIndex );
212
+ currentIndex ++;
213
+ }
214
+
215
+ // Track value argument
216
+ maxUsed = Math .max (maxUsed , currentIndex );
217
+
218
+ return new ProcessResult (formatted , maxUsed );
219
+ }
220
+
221
+ // Track all consumed arguments properly
222
+ FormatArguments args = extractFormatArguments (spec , list , sepArgIndex );
223
+
224
+ // Track width argument if consumed
225
+ if (spec .widthFromArg ) {
226
+ if (spec .widthArgIndex != null ) {
227
+ maxUsed = Math .max (maxUsed , spec .widthArgIndex - 1 );
228
+ } else {
229
+ // Width from sequential position
230
+ int widthPos = sepArgIndex ;
231
+ if (spec .parameterIndex != null ) {
232
+ // For %2$*d, width comes from current sequential position
233
+ widthPos = sepArgIndex ;
234
+ }
235
+ maxUsed = Math .max (maxUsed , widthPos );
236
+ }
237
+ }
238
+
239
+ // Track precision argument if consumed
240
+ if (spec .precisionFromArg ) {
241
+ if (spec .precisionArgIndex != null ) {
242
+ maxUsed = Math .max (maxUsed , spec .precisionArgIndex - 1 );
243
+ } else {
244
+ // Precision from sequential position
245
+ int precPos = sepArgIndex ;
246
+ if (spec .widthFromArg && spec .widthArgIndex == null ) {
247
+ precPos ++; // After width
248
+ }
249
+ if (spec .parameterIndex != null && !spec .widthFromArg ) {
250
+ // For %2$.*d, precision comes from current sequential position
251
+ precPos = sepArgIndex ;
252
+ }
253
+ maxUsed = Math .max (maxUsed , precPos );
254
+ }
255
+ }
256
+
257
+ // Track value argument
258
+ if (args .valueArgIndex < list .size ()) {
259
+ maxUsed = Math .max (maxUsed , args .valueArgIndex );
260
+ }
261
+
262
+ return new ProcessResult (formatted , maxUsed );
263
+ }
264
+
114
265
/**
115
266
* Process a single format specifier.
116
267
*
117
268
* @param spec The parsed format specifier
118
269
* @param list The argument list
119
270
* @param sepArgIndex Current argument index
120
271
* @param formatter The value formatter instance
272
+ * @param maxArgIndexUsed Maximum argument index used so far (for tracking)
121
273
* @return The formatted string
122
274
*/
123
275
private static String processFormatSpecifier (
124
276
FormatSpecifier spec ,
125
277
RuntimeList list ,
126
278
int sepArgIndex ,
127
- SprintfValueFormatter formatter ) {
279
+ SprintfValueFormatter formatter ,
280
+ int maxArgIndexUsed ) {
128
281
129
282
// System.err.println("DEBUG processFormatSpecifier: raw='" + spec.raw + "', isValid=" + spec.isValid + ", errorMessage=" + spec.errorMessage);
130
283
@@ -209,6 +362,17 @@ private static String processFormatSpecifier(
209
362
}
210
363
}
211
364
365
+ /**
366
+ * Process without tracking (for compatibility).
367
+ */
368
+ private static String processFormatSpecifier (
369
+ FormatSpecifier spec ,
370
+ RuntimeList list ,
371
+ int sepArgIndex ,
372
+ SprintfValueFormatter formatter ) {
373
+ return processFormatSpecifier (spec , list , sepArgIndex , formatter , -1 );
374
+ }
375
+
212
376
/**
213
377
* Extract width, precision, and value index from the format specifier.
214
378
*/
0 commit comments