-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclap_20260614.html
More file actions
761 lines (704 loc) · 191 KB
/
Copy pathclap_20260614.html
File metadata and controls
761 lines (704 loc) · 191 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Code Audit — Interactive</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;font-size:14px;line-height:1.5}
.app{max-width:1100px;margin:0 auto;padding:1.5rem 1rem}
.top{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}
h1{font-size:1.5rem;font-weight:700;color:#f1f5f9}
.meta{font-size:.8rem;color:#64748b;margin-top:.2rem}
.top-right{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
.project-info{margin-bottom:1rem;padding:.75rem .9rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.project-info .pi-name{font-size:.95rem;font-weight:700;color:#f1f5f9}
.project-info .pi-desc{font-size:.82rem;color:#cbd5e1;margin-top:.25rem}
.project-info .pi-row{display:flex;flex-wrap:wrap;gap:.4rem .9rem;margin-top:.45rem;font-size:.78rem;color:#94a3b8}
.project-info .pi-row .pi-k{color:#64748b}
.project-info a{color:#60a5fa;text-decoration:none}
.project-info a:hover{text-decoration:underline}
.project-info code{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.74rem;color:#93c5fd}
.pi-repo-btn{display:inline-flex;align-items:center;gap:.3rem;padding:.18rem .5rem;background:#0f172a;border:1px solid #3b82f6;border-radius:6px;color:#60a5fa !important;font-weight:600}
.pi-repo-btn:hover{background:#1d283a;text-decoration:none !important}
#search{background:#1e293b;border:1px solid #334155;color:#e2e8f0;padding:.4rem .75rem;border-radius:6px;font-size:.875rem;width:220px;outline:none}
#search:focus{border-color:#3b82f6}
.group-label{font-size:.8rem;color:#94a3b8;display:flex;align-items:center;gap:.4rem}
select{background:#1e293b;border:1px solid #334155;color:#e2e8f0;padding:.35rem .6rem;border-radius:6px;font-size:.8rem;outline:none;cursor:pointer}
select:focus{border-color:#3b82f6}
.filterbar{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.6rem;padding:.6rem .75rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.filter-row{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center}
.filter-lbl{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#475569;white-space:nowrap;margin-right:.25rem}
.filter-toggle{font-size:.65rem;font-weight:600;padding:.18rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#94a3b8;cursor:pointer;margin-left:auto;white-space:nowrap;letter-spacing:.04em}
.filter-toggle:hover{background:#1e293b;color:#e2e8f0;border-color:#475569}
.badge{font-size:.68rem;font-weight:700;padding:.18rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;letter-spacing:.03em;transition:opacity .12s,filter .12s;user-select:none}
.badge.off{opacity:.28;filter:grayscale(.5)}
.sev-critical{background:#450a0a;color:#fca5a5;border-color:#991b1b}
.sev-high {background:#431407;color:#fdba74;border-color:#9a3412}
.sev-medium {background:#422006;color:#fcd34d;border-color:#854d0e}
.sev-low {background:#0c1a3a;color:#93c5fd;border-color:#1d4ed8}
.sev-negligible{background:#1e293b;color:#64748b;border-color:#334155}
.type-badge{background:#0c1e33;color:#7dd3fc;border-color:#0369a1}
.easy-badge{background:#052e16;color:#86efac;border-color:#16a34a}
.st-open{background:#1e293b;color:#475569;border-color:#334155}
.st-fixed{background:#052e16;color:#4ade80;border-color:#16a34a}
.st-wont{background:#431407;color:#fdba74;border-color:#9a3412}
.st-fp{background:#2e1065;color:#c4b5fd;border-color:#7c3aed}
#progress-wrap{display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem;flex-wrap:wrap}
#progress-bar{flex:1;min-width:120px;height:7px;background:#1e293b;border-radius:4px;overflow:hidden;border:1px solid #334155}
#progress-fill{height:100%;background:#22c55e;border-radius:4px;transition:width .35s ease;width:0}
#progress-text{font-size:.75rem;color:#64748b;white-space:nowrap}
.action-btn{font-size:.72rem;padding:.25rem .6rem;border-radius:5px;border:1px solid #334155;background:#1e293b;color:#94a3b8;cursor:pointer;white-space:nowrap}
.action-btn:hover{background:#334155;color:#e2e8f0}
#hidden-bar{margin-bottom:.6rem}
.hidden-inner{display:flex;flex-wrap:wrap;gap:.35rem;align-items:center}
.hidden-lbl{font-size:.72rem;color:#475569}
.show-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#1e293b;color:#94a3b8;border:1px solid #334155;cursor:pointer}
.show-btn:hover{background:#334155;color:#e2e8f0}
.show-all-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#2563eb;color:#fff;border:none;cursor:pointer}
.show-all-btn:hover{background:#1d4ed8}
.group-section{margin-bottom:1.5rem}
.group-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;padding-bottom:.35rem;border-bottom:1px solid #334155}
.group-title{font-size:.95rem;font-weight:700;color:#cbd5e1}
.group-title.all-done{color:#4ade80}
.group-count{font-size:.72rem;color:#475569;background:#1e293b;padding:.1rem .4rem;border-radius:10px;white-space:nowrap}
.icon-btn{background:transparent;border:none;color:#334155;cursor:pointer;font-size:.85rem;padding:.05rem .2rem;line-height:1}
.icon-btn:hover{color:#64748b}
.hide-btn{margin-left:auto;font-size:.68rem;padding:.12rem .45rem;border-radius:4px;background:transparent;color:#334155;border:1px solid #1e293b;cursor:pointer}
.hide-btn:hover{background:#1e293b;color:#64748b}
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:.6rem;overflow:hidden;transition:opacity .15s}
.card.resolved{opacity:.55;border-left:3px solid #16a34a}
.card.resolved:hover{opacity:.85}
.card-header{display:flex;align-items:center;gap:.5rem;padding:.45rem .85rem;background:#162032;border-bottom:1px solid #334155;flex-wrap:wrap;cursor:pointer;user-select:none}
.card-header:hover{background:#1a2840}
.card-arrow{font-size:.65rem;color:#334155;flex-shrink:0}
.card-id{font-size:.75rem;font-weight:700;color:#64748b;font-family:monospace;letter-spacing:.04em}
.card-loc{font-size:.72rem;color:#64748b;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.status-btn{margin-left:auto;font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;white-space:nowrap;transition:filter .1s}
.status-btn:hover{filter:brightness(1.25)}
.copy-btn{font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#94a3b8;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s}
.copy-btn:hover{background:#334155;color:#e2e8f0}
.card-body{padding:.85rem .9rem}
.field-lbl{font-size:.85rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#7dd3fc;margin:1rem 0 .35rem;padding-bottom:.18rem;border-bottom:1px solid #1e293b}
.field-lbl:first-child{margin-top:0}
.desc,.fix-txt{font-size:.875rem;color:#cbd5e1;line-height:1.55}
.desc p,.fix-txt p{margin:.4rem 0}
.desc p:first-child,.fix-txt p:first-child{margin-top:0}
.desc p:last-child,.fix-txt p:last-child{margin-bottom:0}
.loc-block{margin:.3rem 0 .6rem}
.loc-ref{font-size:.8rem;color:#38bdf8;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-weight:600;margin-bottom:.22rem}
.loc-lines{color:#475569;font-weight:400}
pre{background:#0f172a;color:#cdd6f4;border-radius:5px;padding:.6rem .8rem;overflow-x:auto;white-space:pre;font-size:.8rem;line-height:1.55;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;border:1px solid #1e293b;max-height:320px}
.desc pre.md-code,.fix-txt pre.md-code{margin:.5rem 0;font-size:.82rem;color:#e2e8f0;background:#0b1224;border:1px solid #1e293b;max-height:none}
code.md-inline{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.86em;background:#0b1224;color:#fca5a5;border:1px solid #1e293b;border-radius:3px;padding:.04rem .32rem;white-space:pre-wrap;word-break:break-word}
.not-found{font-size:.8rem;color:#334155;font-style:italic}
mark{background:#854d0e;color:#fef9c3;border-radius:2px;padding:0 .1rem}
.no-results{text-align:center;color:#334155;padding:3rem 1rem;font-size:.9rem}
</style>
</head>
<body>
<script type="application/json" id="D">[{"id":"PANIC_1","type":"panic","type_label":"Panic","severity":"critical","easy":true,"description":"In `resolve_pending`, `self.cmd.find(&pending.id).expect(INTERNAL_ERROR_MSG)` panics if the pending arg id is somehow not found in the command. While clap intends this to be impossible via the INTERNAL_ERROR_MSG convention, the panic happens on the hot path for every argument resolution, with no way for callers to recover. If a bug elsewhere allows a stale or mangled `pending.id` to be set, parsing will panic instead of returning an error. The `add_val_to` and `add_index_to` methods in `ArgMatcher` also call `.expect(INTERNAL_ERROR_MSG)` after a get_mut that is always preceded by a matching insert, but any ordering bug would yield a panic.","fix":"These panics are acceptable given the invariant, but they should all be guarded so that `debug_assert!` + a fallback `ClapError::internal` path is taken in release builds, making user-visible panics impossible.","locations":[{"ref":"clap_builder/src/parser/parser.rs:1128","code":" 1128 | let arg = self.cmd.find(&pending.id).expect(INTERNAL_ERROR_MSG);","lines":1},{"ref":"clap_builder/src/parser/arg_matcher.rs:169","code":" 169 | let ma = self.get_mut(arg).expect(INTERNAL_ERROR_MSG);","lines":1},{"ref":"clap_builder/src/parser/arg_matcher.rs:174","code":" 174 | let ma = self.get_mut(arg).expect(INTERNAL_ERROR_MSG);","lines":1}]},{"id":"PANIC_2","type":"panic","type_label":"Panic","severity":"high","easy":false,"description":"`get_arg_conflicts_with()` is a public method that panics when a conflict ID refers to an arg that\ndoes not exist in the command. The method calls `.unwrap_or_else(|e| panic!(\"{e}\"))` inside a loop\nover conflict IDs, so any caller passing an `Arg` whose `conflicts_with` list contains a stale or\nmistyped ID gets a hard process abort with no opportunity for the caller to recover. Because the\nmethod is public and takes a shared reference, callers have no way to validate the input first.","fix":"Return `Result<Vec<&Arg>, Error>` or silently skip unknown IDs (matching the more forgiving\napproach used elsewhere). At minimum, replace the panic with `continue` or a structured error so\ncallers can handle stale conflict references gracefully.","locations":[{"ref":"clap_builder/src/builder/command.rs:4031-4054","code":" 4031 | pub fn get_arg_conflicts_with(&self, arg: &Arg) -> Vec<&Arg> // FIXME: This could probably have been an iterator\n 4032 | {\n 4033 | if arg.is_global_set() {\n 4034 | self.get_global_arg_conflicts_with(arg)\n 4035 | } else {\n 4036 | let mut result = Vec::new();\n 4037 | for id in arg.blacklist.iter() {\n 4038 | if let Some(arg) = self.find(id) {\n 4039 | result.push(arg);\n 4040 | } else if let Some(group) = self.find_group(id) {\n 4041 | result.extend(\n 4042 | self.unroll_args_in_group(&group.id)\n 4043 | .iter()\n 4044 | .map(|id| self.find(id).expect(INTERNAL_ERROR_MSG)),\n 4045 | );\n 4046 | } else {\n 4047 | panic!(\n 4048 | \"Command::get_arg_conflicts_with: The passed arg conflicts with an arg unknown to the cmd\"\n 4049 | );\n 4050 | }\n 4051 | }\n 4052 | result\n 4053 | }\n 4054 | }","lines":24}]},{"id":"PANIC_3","type":"panic","type_label":"Panic","severity":"high","easy":false,"description":"`get_global_arg_conflicts_with()` calls `.expect(INTERNAL_ERROR_MSG)` inside a `.map()` closure\nthat looks up args by ID. If a conflict ID stored on the arg does not resolve to a known arg in\nthe command, the expect panics. This is a public method, so library users who build commands with\ncross-subcommand conflict references can trigger the panic at runtime during help generation or\nvalidation logic.","fix":"Replace `.expect()` with `?` / `filter_map` to skip or propagate unknown IDs rather than\naborting. Mirror the fix suggested for `get_arg_conflicts_with`.","locations":[{"ref":"clap_builder/src/builder/command.rs:4065-4084","code":" 4065 | fn get_global_arg_conflicts_with(&self, arg: &Arg) -> Vec<&Arg> // FIXME: This could probably have been an iterator\n 4066 | {\n 4067 | arg.blacklist\n 4068 | .iter()\n 4069 | .map(|id| {\n 4070 | self.args\n 4071 | .args()\n 4072 | .chain(\n 4073 | self.get_subcommands_containing(arg)\n 4074 | .iter()\n 4075 | .flat_map(|x| x.args.args()),\n 4076 | )\n 4077 | .find(|arg| arg.get_id() == id)\n 4078 | .expect(\n 4079 | \"Command::get_arg_conflicts_with: \\\n 4080 | The passed arg conflicts with an arg unknown to the cmd\",\n 4081 | )\n 4082 | })\n 4083 | .collect()\n 4084 | }","lines":20}]},{"id":"PANIC_4","type":"panic","type_label":"Panic","severity":"high","easy":true,"description":"`append_val` in `MatchedArg` calls `.expect(INTERNAL_ERROR_MSG)` on `last_mut()` of both `self.vals` and `self.raw_vals`. If `new_val_group` was not called before `append_val` (which must always be called first), both vecs will be empty and `last_mut()` returns `None`, causing a panic. This invariant is maintained internally but is purely runtime-enforced and silently breaks with `#[allow(dead_code)]` on `num_vals_last_group`.","fix":"Assert the invariant with `debug_assert!(!self.vals.is_empty(), ...)` and consider making `append_val` private and only callable via a type that guarantees a group exists.","locations":[{"ref":"clap_builder/src/parser/matches/matched_arg.rs:117-122","code":" 117 | // We assume there is always a group created before.\n 118 | self.vals.last_mut().expect(INTERNAL_ERROR_MSG).push(val);\n 119 | self.raw_vals\n 120 | .last_mut()\n 121 | .expect(INTERNAL_ERROR_MSG)\n 122 | .push(raw_val);","lines":6},{"ref":"clap_builder/src/parser/matches/matched_arg.rs:129-133","code":" 129 | // Will be used later\n 130 | #[allow(dead_code)]\n 131 | pub(crate) fn num_vals_last_group(&self) -> usize {\n 132 | self.vals.last().map(|x| x.len()).unwrap_or(0)\n 133 | }","lines":5}]},{"id":"PANIC_5","type":"panic","type_label":"Panic","severity":"medium","easy":false,"description":"`Arg::get_min_vals()` calls `.expect(INTERNAL_ERROR_MSG)` on the result of `self.num_vals.clone()`\nwhich is `None` until `Arg::_build()` is called during command building. Any caller that invokes\n`get_min_vals()` on an `Arg` that has not yet been embedded in a built `Command` will get a panic.\nThe method is public and its signature gives no indication of the build-phase precondition.","fix":"Return `Option<usize>` (or `usize` with a documented 0 default) instead of panicking. If a build\ninvariant is truly required, add a `#[doc(hidden)]` note or rename the method to signal that it is\nonly valid post-build.","locations":[{"ref":"clap_builder/src/builder/arg.rs:4353-4356","code":" 4353 | #[inline]\n 4354 | pub(crate) fn get_min_vals(&self) -> usize {\n 4355 | self.get_num_args().expect(INTERNAL_ERROR_MSG).min_values()\n 4356 | }","lines":4}]},{"id":"PANIC_6","type":"panic","type_label":"Panic","severity":"medium","easy":false,"description":"EnumValueParser::parse_ref calls .expect() on to_possible_value() inside the find() predicate closure (value_parser.rs line 1122-1124) and ValueEnum::from_str does the same (derive.rs line 302-304). If a ValueEnum implementation returns None from to_possible_value() for a non-skipped variant - which the contract warns against but cannot enforce at compile time - this panics during argument parsing rather than returning an error, crashing the application with no useful context.","fix":"In the find closure, use filter_map(|v| v.to_possible_value()) to skip None variants instead of expect(). This matches the intent of 'skipped' variants documented in the trait and avoids the panic. Alternatively, add a debug_assert before the find call to validate the contract.","locations":[{"ref":"clap_builder/src/builder/value_parser.rs:1119-1135","code":" 1119 | let value = ok!(E::value_variants()\n 1120 | .iter()\n 1121 | .find(|v| {\n 1122 | v.to_possible_value()\n 1123 | .expect(\"ValueEnum::value_variants contains only values with a corresponding ValueEnum::to_possible_value\")\n 1124 | .matches(value, ignore_case)\n 1125 | })\n 1126 | .ok_or_else(|| {\n 1127 | crate::Error::invalid_value(\n 1128 | cmd,\n 1129 | value.to_owned(),\n 1130 | &possible_vals(),\n 1131 | arg.map(ToString::to_string)\n 1132 | .unwrap_or_else(|| \"...\".to_owned()),\n 1133 | )\n 1134 | }))\n 1135 | .clone();","lines":17},{"ref":"clap_builder/src/derive.rs:299-307","code":" 299 | Self::value_variants()\n 300 | .iter()\n 301 | .find(|v| {\n 302 | v.to_possible_value()\n 303 | .expect(\"ValueEnum::value_variants contains only values with a corresponding ValueEnum::to_possible_value\")\n 304 | .matches(input, ignore_case)\n 305 | })\n 306 | .cloned()\n 307 | .ok_or_else(|| format!(\"invalid variant: {input}\"))","lines":9}]},{"id":"PANIC_7","type":"panic","type_label":"Panic","severity":"medium","easy":true,"description":"In `HelpTemplate::help`, the `.expect(\"Only called with possible value\")`\nat line 675 panics when all possible values are hidden (every entry has\n`is_hide_set()` returning `true`). The guard at line 666 only checks that\n`possible_vals` is non-empty, but then the iterator at line 672 filters with\n`.filter(|f| !f.is_hide_set())`. If every value is hidden, the filtered\niterator is empty and `.max()` returns `None`, causing the `expect` to panic.","fix":"Replace the `expect` with a `match` that skips the block if no visible values\nremain:\n let longest = match possible_vals\n .iter()\n .filter(|f| !f.is_hide_set())\n .map(|f| display_width(f.get_name()))\n .max()\n {\n Some(l) => l,\n None => continue, // all values are hidden\n };","locations":[{"ref":"clap_builder/src/output/help_template.rs:662-675","code":" 662 | if let Some(arg) = arg {\n 663 | if !arg.is_hide_possible_values_set() && self.use_long_pv(arg) {\n 664 | const DASH_SPACE: usize = \"- \".len();\n 665 | let possible_vals = arg.get_possible_values();\n 666 | if !possible_vals.is_empty() {\n 667 | debug!(\"HelpTemplate::help: Found possible vals...{possible_vals:?}\");\n 668 | has_possible_values = true;\n 669 | \n 670 | let longest = possible_vals\n 671 | .iter()\n 672 | .filter(|f| !f.is_hide_set())\n 673 | .map(|f| display_width(f.get_name()))\n 674 | .max()\n 675 | .expect(\"Only called with possible value\");","lines":14}]},{"id":"PANIC_8","type":"panic","type_label":"Panic","severity":"medium","easy":true,"description":"`Name::translate_char` panics with `.unwrap()` (line 1475) when the\nidentifier string, after casing transformation, is empty. While standard\nRust identifiers cannot be empty, a raw identifier such as `r#_` can produce\nan empty string after some casing transformations (e.g. `to_kebab_case` on a\nsingle underscore yields `\"\"`). The `unwrap` at line 1461 in the\n`Assigned` branch is also emitted literally into user-generated code:\n`(#tokens).chars().next().unwrap()` - if the user passes an expression that\nevaluates to an empty string at runtime the generated binary panics.","fix":"For `Derived`: replace the bare `.unwrap()` with a compile-time assertion or a\n`proc_macro_error` abort:\n let s = s.chars().next().unwrap_or_else(|| {\n abort!(ident, \"cannot derive a short flag from an empty identifier\");\n });\nFor `Assigned`: emit `.chars().next().expect(\"short flag expression must not be empty\")` with a\ndescriptive message, or validate at macro expansion time.","locations":[{"ref":"clap_derive/src/item.rs:1457-1479","code":" 1457 | pub(crate) fn translate_char(self, style: CasingStyle) -> TokenStream {\n 1458 | use CasingStyle::{Camel, Kebab, Lower, Pascal, ScreamingSnake, Snake, Upper, Verbatim};\n 1459 | \n 1460 | match self {\n 1461 | Name::Assigned(tokens) => quote!( (#tokens).chars().next().unwrap() ),\n 1462 | Name::Derived(ident) => {\n 1463 | let s = ident.unraw().to_string();\n 1464 | let s = match style {\n 1465 | Pascal => s.to_upper_camel_case(),\n 1466 | Kebab => s.to_kebab_case(),\n 1467 | Camel => s.to_lower_camel_case(),\n 1468 | ScreamingSnake => s.to_shouty_snake_case(),\n 1469 | Snake => s.to_snake_case(),\n 1470 | Lower => s.to_snake_case(),\n 1471 | Upper => s.to_shouty_snake_case(),\n 1472 | Verbatim => s,\n 1473 | };\n 1474 | \n 1475 | let s = s.chars().next().unwrap();\n 1476 | quote_spanned!(ident.span()=> #s)\n 1477 | }\n 1478 | }\n 1479 | }","lines":23}]},{"id":"PANIC_9","type":"panic","type_label":"Panic","severity":"low","easy":true,"description":"`gen_from_arg_matches` (subcommand.rs) emits `.unwrap()` in generated user\ncode at the line that chains external-subcommand extra arguments:\n\n #sub_arg_matches_var.remove_many::<#str_ty>(\"\").unwrap()\n\nIf an external subcommand is invoked with no trailing arguments, clap may not\npopulate the `\"\"` key at all, so `remove_many` could return `None`. The\ngenerated `unwrap()` would then panic in the user's binary instead of\npropagating a proper `clap::Error`.","fix":"Replace `.unwrap()` with `.unwrap_or_default()` or chain an iterator that\nhandles the `None` case:\n .chain(\n #sub_arg_matches_var\n .remove_many::<#str_ty>(\"\")\n .into_iter()\n .flatten()\n .map(#str_ty::from)\n )","locations":[{"ref":"clap_derive/src/derives/subcommand.rs:537-549","code":" 537 | let wildcard = match ext_subcmd {\n 538 | Some((span, var_name, str_ty)) => quote_spanned! { span=>\n 539 | ::std::result::Result::Ok(Self::#var_name(\n 540 | ::std::iter::once(#str_ty::from(#subcommand_name_var))\n 541 | .chain(\n 542 | #sub_arg_matches_var\n 543 | .remove_many::<#str_ty>(\"\")\n 544 | .unwrap()\n 545 | .map(#str_ty::from)\n 546 | )\n 547 | .collect::<::std::vec::Vec<_>>()\n 548 | ))\n 549 | },","lines":13}]},{"id":"LOGIC_1","type":"logic_error","type_label":"Logic Error","severity":"high","easy":false,"description":"In `get_matches_with`, when `is_ignore_errors_set()` is true and parsing fails, both `add_env` and `add_defaults` are called inside the `inspect_err` closure (lines 62-64). Then after the `ok!` macro propagates the error, `add_env` and `add_defaults` are called AGAIN on lines 69-70 for the non-error path. If the error is then swallowed by the ignore-errors mechanism at a higher level, defaults may be double-applied. The `inspect_err` block's `add_env`/`add_defaults` results are discarded with `let _ =`, so any errors they produce are silently lost.","fix":"Move the `add_env`/`add_defaults` calls out of `inspect_err` entirely. In the error path, apply defaults only once after swallowing the error, not inside `inspect_err`.","locations":[{"ref":"clap_builder/src/parser/parser.rs:57-70","code":" 57 | ok!(self\n 58 | .parse(matcher, raw_args, args_cursor)\n 59 | .inspect_err(|_err| {\n 60 | if self.cmd.is_ignore_errors_set() {\n 61 | #[cfg(feature = \"env\")]\n 62 | let _ = self.add_env(matcher);\n 63 | let _ = self.add_defaults(matcher);\n 64 | }\n 65 | }));\n 66 | ok!(self.resolve_pending(matcher));\n 67 | \n 68 | #[cfg(feature = \"env\")]\n 69 | ok!(self.add_env(matcher));\n 70 | ok!(self.add_defaults(matcher));","lines":14}]},{"id":"LOGIC_2","type":"logic_error","type_label":"Logic Error","severity":"high","easy":false,"description":"In `parse_subcommand`, when `_build_subcommand` returns `None` (which can happen when the subcommand was not found), the function silently returns `Ok(())` without recording a subcommand in the matcher. Callers that broke out of the main loop with a valid `subcmd_name` assume it refers to a real subcommand (via the earlier `.expect(INTERNAL_ERROR_MSG)` at line 500 of `parser.rs`), but if `_build_subcommand` returns `None` after that, the subcommand is silently dropped from the parse result with no error.","fix":"Add a `debug_assert!(sc.is_some(), ...)` or an explicit internal error when `_build_subcommand` returns `None` after a subcommand name was confirmed to exist.","locations":[{"ref":"clap_builder/src/parser/parser.rs:728-759","code":" 728 | if let Some(sc) = self.cmd._build_subcommand(sc_name) {\n 729 | let mut sc_matcher = ArgMatcher::new(sc);\n 730 | \n 731 | debug!(\n 732 | \"Parser::parse_subcommand: About to parse sc={}\",\n 733 | sc.get_name()\n 734 | );\n 735 | \n 736 | {\n 737 | let mut p = Parser::new(sc);\n 738 | // HACK: maintain indexes between parsers\n 739 | // FlagSubCommand short arg needs to revisit the current short args, but skip the subcommand itself\n 740 | if keep_state {\n 741 | p.cur_idx.set(self.cur_idx.get());\n 742 | p.flag_subcmd_at = self.flag_subcmd_at;\n 743 | p.flag_subcmd_skip = self.flag_subcmd_skip;\n 744 | }\n 745 | if let Err(error) = p.get_matches_with(&mut sc_matcher, raw_args, args_cursor) {\n 746 | if partial_parsing_enabled && error.use_stderr() {\n 747 | debug!(\n 748 | \"Parser::parse_subcommand: ignored error in subcommand {sc_name}: {error:?}\"\n 749 | );\n 750 | } else {\n 751 | return Err(error);\n 752 | }\n 753 | }\n 754 | }\n 755 | matcher.subcommand(SubCommand {\n 756 | name: sc.get_name().to_owned(),\n 757 | matches: sc_matcher.into_inner(),\n 758 | });\n 759 | }","lines":32},{"ref":"clap_builder/src/parser/parser.rs:497-503","code":" 497 | let sc_name = self\n 498 | .cmd\n 499 | .find_subcommand(pos_sc_name)\n 500 | .expect(INTERNAL_ERROR_MSG)\n 501 | .get_name()\n 502 | .to_owned();\n 503 | ok!(self.parse_subcommand(&sc_name, matcher, raw_args, args_cursor, keep_state));","lines":7}]},{"id":"LOGIC_3","type":"logic_error","type_label":"Logic Error","severity":"high","easy":true,"description":"The bash generator `all_subcommands` emits command names and parent function names unescaped inside a `case` pattern (line 124: `{parent_fn_name},{name})`). A subcommand name containing `)` or `*` or `|` or `#` will produce syntactically invalid or unintentionally matching bash `case` patterns, allowing injection into the completion script.","fix":"Validate or sanitize subcommand names before embedding them in case patterns, or escape pattern metacharacters.","locations":[{"ref":"clap_complete/src/aot/shells/bash.rs:121-130","code":" 121 | let mut cases = vec![String::new()];\n 122 | for (parent_fn_name, name, fn_name) in subcmds {\n 123 | cases.push(format!(\n 124 | \"{parent_fn_name},{name})\n 125 | cmd=\\\"{fn_name}\\\"\n 126 | ;;\",\n 127 | ));\n 128 | }\n 129 | \n 130 | cases.join(\"\\n \")","lines":10}]},{"id":"LOGIC_4","type":"logic_error","type_label":"Logic Error","severity":"high","easy":true,"description":"The bash generator `vals_for` function emits possible values directly into a double-quoted string inside `compgen -W \"...\"` without any shell escaping. If a possible value contains a space or a double-quote character, the resulting `compgen -W` invocation is syntactically broken or splits on whitespace, producing wrong completions. For example, a value of `foo bar` becomes two words in compgen instead of one.","fix":"Shell-quote each value name before joining. Wrap every value in single-quotes and escape any single-quotes within the name (replace `'` with `'\"'\"'`), then join the escaped names with spaces.","locations":[{"ref":"clap_complete/src/aot/shells/bash.rs:258-266","code":" 258 | if let Some(vals) = utils::possible_values(o) {\n 259 | format!(\n 260 | \"$(compgen -W \\\"{}\\\" -- \\\"${{cur}}\\\")\",\n 261 | vals.iter()\n 262 | .filter(|pv| !pv.is_hide_set())\n 263 | .map(|n| n.get_name())\n 264 | .collect::<Vec<_>>()\n 265 | .join(\" \")\n 266 | )","lines":9}]},{"id":"LOGIC_5","type":"logic_error","type_label":"Logic Error","severity":"high","easy":true,"description":"The bash generator `all_options_for_path` writes possible values and subcommand names directly into the opts string that is later embedded in `compgen -W \"\\${opts}\"` (see template at line 60/68). Values or subcommand names containing spaces or shell metacharacters break word-splitting inside compgen, either producing wrong completions or breaking the script entirely.","fix":"Shell-quote every value name and subcommand name before appending it to opts, using the same single-quote escaping strategy as needed for bash double-quoted contexts.","locations":[{"ref":"clap_complete/src/aot/shells/bash.rs:276-303","code":" 276 | fn all_options_for_path(cmd: &Command, path: &str) -> String {\n 277 | debug!(\"all_options_for_path: path={path}\");\n 278 | \n 279 | let p = utils::find_subcommand_with_path(cmd, path.split(CMD_SEP).skip(1).collect());\n 280 | \n 281 | let mut opts = String::new();\n 282 | for short in utils::shorts_and_visible_aliases(p) {\n 283 | write!(&mut opts, \"-{short} \").expect(\"writing to String is infallible\");\n 284 | }\n 285 | for long in utils::longs_and_visible_aliases(p) {\n 286 | write!(&mut opts, \"--{long} \").expect(\"writing to String is infallible\");\n 287 | }\n 288 | for pos in p.get_positionals() {\n 289 | if let Some(vals) = utils::possible_values(pos) {\n 290 | for value in vals {\n 291 | write!(&mut opts, \"{} \", value.get_name())\n 292 | .expect(\"writing to String is infallible\");\n 293 | }\n 294 | } else {\n 295 | write!(&mut opts, \"{pos} \").expect(\"writing to String is infallible\");\n 296 | }\n 297 | }\n 298 | for (sc, _) in utils::subcommands(p) {\n 299 | write!(&mut opts, \"{sc} \").expect(\"writing to String is infallible\");\n 300 | }\n 301 | opts.pop();\n 302 | \n 303 | opts","lines":28}]},{"id":"LOGIC_6","type":"logic_error","type_label":"Logic Error","severity":"high","easy":false,"description":"The elvish generator `generate_inner` embeds the `command_name` (built from bin name and subcommand chain) directly in an Elvish single-quoted map key (`&'...'=`) at line 134-138 without passing it through `escape_string`. If the binary name or a subcommand name contains a single-quote (`'`), the generated Elvish code has unbalanced quotes and is invalid.","fix":"Apply `escape_string(command_name)` (which replaces `'` with `''`) when embedding `command_name` in the map-key literal at line 135.","locations":[{"ref":"clap_complete/src/aot/shells/elvish.rs:131-139","code":" 131 | let mut subcommands_cases = String::new();\n 132 | for command_name in &command_names {\n 133 | subcommands_cases.push_str(&format!(\n 134 | r\"\n 135 | &'{}'= {{{}\n 136 | }}\",\n 137 | &command_name, completions\n 138 | ));\n 139 | }","lines":9}]},{"id":"LOGIC_7","type":"logic_error","type_label":"Logic Error","severity":"high","easy":false,"description":"The zsh generator `subcommands_of` (via `add_subcommands`) places the subcommand name directly in the single-quoted zsh string `'name:help'` at line 162-163 without escaping the name. If the name contains `'`, `\\`, or `:`, the generated zsh completion function has a syntax error or splits the name/help incorrectly. The `escape_help` function is used for the help portion but nothing escapes the `name` portion.","fix":"Apply an analogous escape function to the `name` argument that at minimum handles `'`, `\\`, and `:` before embedding it in the single-quoted pattern.","locations":[{"ref":"clap_complete/src/aot/shells/zsh.rs:159-169","code":" 159 | fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) {\n 160 | debug!(\"add_subcommands\");\n 161 | \n 162 | let text = format!(\n 163 | \"'{name}:{help}' \\\\\",\n 164 | name = name,\n 165 | help = escape_help(&subcommand.get_about().unwrap_or_default().to_string())\n 166 | );\n 167 | \n 168 | ret.push(text);\n 169 | }","lines":11}]},{"id":"LOGIC_8","type":"logic_error","type_label":"Logic Error","severity":"high","easy":false,"description":"In `env/shells.rs`, the bash, zsh, and elvish registration scripts are generated via chained `.replace()` calls on a raw string template. The substitution values (`bin`, `completer`, `var`, `escaped_name`) are inserted one-by-one, so a value substituted earlier can be matched and corrupted by a later replacement. For example, in the bash template the replacement order is NAME->BIN->COMPLETER->VAR: if `bin` contains the literal text `COMPLETER`, the subsequent replacement step will corrupt it. Similarly if `completer` contains `VAR`. This is a classic cascaded find-and-replace injection vulnerability.","fix":"Use a single-pass templating approach (e.g. format strings with named arguments, or replace with unique sentinel strings that cannot appear in user-supplied values). Alternatively, build the script string piece-by-piece with string concatenation rather than chained text replacement, so that substituted values cannot be re-matched by subsequent replacements.","locations":[{"ref":"clap_complete/src/env/shells.rs:64-67","code":" 64 | .replace(\"NAME\", &escaped_name)\n 65 | .replace(\"BIN\", bin)\n 66 | .replace(\"COMPLETER\", &completer)\n 67 | .replace(\"VAR\", var);","lines":4},{"ref":"clap_complete/src/env/shells.rs:165-168","code":" 165 | \"#\n 166 | .replace(\"COMPLETER\", &completer)\n 167 | .replace(\"BIN\", &bin)\n 168 | .replace(\"VAR\", var);","lines":4},{"ref":"clap_complete/src/env/shells.rs:457-460","code":" 457 | .replace(\"NAME\", &escaped_name)\n 458 | .replace(\"COMPLETER\", &completer)\n 459 | .replace(\"BIN\", &bin)\n 460 | .replace(\"VAR\", var);","lines":4}]},{"id":"LOGIC_9","type":"logic_error","type_label":"Logic Error","severity":"high","easy":true,"description":"`is_number(\"\")` returns `true` because the loop body never executes and\n`position_of_e` is `None`, so the function falls through to `None => true`.\nAs a consequence, `ParsedArg::is_negative_number()` returns `true` for the\nargument `\"-\"` (stdin marker): `\"-\".strip_prefix('-')` yields `Some(\"\")`,\nand `is_number(\"\")` is `true`. While callers typically check `is_stdio()`\nfirst, the predicate itself is semantically wrong - an empty string is not a\nnumber - and could mislead any future caller that relies on the function in\nisolation.","fix":"Add a short-circuit guard at the start of `is_number`:\n if arg.is_empty() { return false; }","locations":[{"ref":"clap_lex/src/lib.rs:492-522","code":" 492 | fn is_number(arg: &str) -> bool {\n 493 | // Return true if this looks like an integer or a float where it's all\n 494 | // digits plus an optional single dot after some digits.\n 495 | //\n 496 | // For floats allow forms such as `1.`, `1.2`, `1.2e10`, etc.\n 497 | let mut seen_dot = false;\n 498 | let mut position_of_e = None;\n 499 | for (i, c) in arg.as_bytes().iter().enumerate() {\n 500 | match c {\n 501 | // Digits are always valid\n 502 | b'0'..=b'9' => {}\n 503 | \n 504 | // Allow a `.`, but only one, only if it comes before an\n 505 | // optional exponent, and only if it's not the first character.\n 506 | b'.' if !seen_dot && position_of_e.is_none() && i > 0 => seen_dot = true,\n 507 | \n 508 | // Allow an exponent `e`/`E` but only at most one after the first\n 509 | // character.\n 510 | b'e' | b'E' if position_of_e.is_none() && i > 0 => position_of_e = Some(i),\n 511 | \n 512 | _ => return false,\n 513 | }\n 514 | }\n 515 | \n 516 | // Disallow `-1e` which isn't a valid float since it doesn't actually have\n 517 | // an exponent.\n 518 | match position_of_e {\n 519 | Some(i) => i != arg.len() - 1,\n 520 | None => true,\n 521 | }\n 522 | }","lines":31}]},{"id":"LOGIC_10","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"`Arg::visible_alias(None)` is intended to clear only visible aliases, but the underlying\n`aliases` field stores both visible and hidden aliases as `(name, is_visible)` tuples. Passing\n`None` to `visible_alias()` calls the `IntoResettable` reset path which sets `self.aliases = None`,\nwiping all aliases - including hidden ones that were set via `alias()`. The same problem exists for\n`visible_short_alias(None)` and `alias(None)`. The asymmetry means that resetting a visible alias\nsilently destroys unrelated hidden aliases with no warning.","fix":"When resetting via `None`, filter out only the aliases that match the visibility level being\nreset rather than setting the entire `aliases` / `short_aliases` field to `None`. Alternatively,\ndocument the behavior explicitly so callers are aware of the combined-reset effect.","locations":[{"ref":"clap_builder/src/builder/arg.rs:254-261","code":" 254 | pub fn alias(mut self, name: impl IntoResettable<Str>) -> Self {\n 255 | if let Some(name) = name.into_resettable().into_option() {\n 256 | self.aliases.push((name, false));\n 257 | } else {\n 258 | self.aliases.clear();\n 259 | }\n 260 | self\n 261 | }","lines":8},{"ref":"clap_builder/src/builder/arg.rs:375-382","code":" 375 | pub fn visible_alias(mut self, name: impl IntoResettable<Str>) -> Self {\n 376 | if let Some(name) = name.into_resettable().into_option() {\n 377 | self.aliases.push((name, true));\n 378 | } else {\n 379 | self.aliases.clear();\n 380 | }\n 381 | self\n 382 | }","lines":8},{"ref":"clap_builder/src/builder/arg.rs:403-412","code":" 403 | #[must_use]\n 404 | pub fn visible_short_alias(mut self, name: impl IntoResettable<char>) -> Self {\n 405 | if let Some(name) = name.into_resettable().into_option() {\n 406 | debug_assert!(name != '-', \"short alias name cannot be `-`\");\n 407 | self.short_aliases.push((name, true));\n 408 | } else {\n 409 | self.short_aliases.clear();\n 410 | }\n 411 | self\n 412 | }","lines":10}]},{"id":"LOGIC_11","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"`_build_subcommand()` constructs `usage_name` (the value used as the subcommand's binary name for\nusage strings) with a fallback that uses the bare `sc_names` without the parent binary name prefix\nwhen `self.bin_name` is `None`. In contrast, `_build_bin_names_internal()` always prepends\n`self_bin_name`. This means help/usage output for subcommands reached through the\n`_build_subcommand` path can show un-prefixed names such as \"sub\" instead of \"prog sub\", making\nusage strings inconsistent with what `_build_bin_names_internal` would produce for the same tree.","fix":"Change the `_build_subcommand` fallback to mirror `_build_bin_names_internal`: always prepend the\nparent's `self_bin_name` (or the current command's name) before `mid_string + sc_names`, removing\nthe divergent code path.","locations":[{"ref":"clap_builder/src/builder/command.rs:4529-4534","code":" 4529 | let usage_name = self\n 4530 | .bin_name\n 4531 | .as_ref()\n 4532 | .map(|bin_name| format!(\"{bin_name}{mid_string}{sc_names}\"))\n 4533 | .unwrap_or(sc_names);\n 4534 | sc.usage_name = Some(usage_name);","lines":6}]},{"id":"LOGIC_12","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":true,"description":"`display_width` treats every ASCII control character (0x00-0x1F and 0x7F)\nas the start of an ANSI escape sequence and then only terminates that sequence\non the letter `m`. Real ANSI/VT100 CSI sequences end on any uppercase or\nlowercase ASCII letter (A-Z, a-z). Sequences such as `\\x1B[1A` (cursor up),\n`\\x1B[2J` (erase display), or `\\x1B[1K` (erase line) are never terminated\nbecause their final byte is not `m`. After such a sequence the parser stays\nin `control_sequence = true` mode for all remaining characters, causing the\nentire rest of the string to be counted as zero-width. This produces wrong\nalignment in help output whenever argument help strings contain non-SGR ANSI\nsequences (e.g. from a custom `Styles` or embedded hyperlinks).\n\nAdditionally, control characters like `\\t`, `\\n`, `\\x07` (BEL), etc.\nerroneously start a \"control sequence\" even though they are not ESC.","fix":"Only enter control-sequence mode on ESC (\\x1B), not on all ASCII control\ncharacters. Terminate on any ASCII letter, not just `m`:\n\n if ch == '\\x1b' {\n control_sequence = true;\n } else if control_sequence && ch.is_ascii_alphabetic() {\n control_sequence = false;\n // do NOT count this terminator character\n continue;\n }","locations":[{"ref":"clap_builder/src/output/textwrap/core.rs:55-74","code":" 55 | pub(crate) fn display_width(text: &str) -> usize {\n 56 | let mut width = 0;\n 57 | \n 58 | let mut control_sequence = false;\n 59 | let control_terminate: char = 'm';\n 60 | \n 61 | for ch in text.chars() {\n 62 | if ch.is_ascii_control() {\n 63 | control_sequence = true;\n 64 | } else if control_sequence && ch == control_terminate {\n 65 | control_sequence = false;\n 66 | continue;\n 67 | }\n 68 | \n 69 | if !control_sequence {\n 70 | width += ch_width(ch);\n 71 | }\n 72 | }\n 73 | width\n 74 | }","lines":20}]},{"id":"LOGIC_13","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `ArgMatcher::fill_in_global_values`, the propagation logic compares `parent_ma.source() > ma.source()` (line 67) to decide which value wins. `ValueSource` derives `PartialOrd`/`Ord` with the declaration order `DefaultValue < EnvVariable < CommandLine`. This means a command-line value in a parent will override an env-variable value in a subcommand, and a subcommand's default will be overridden by the parent's env value. This priority is reasonable, but the comparison is subtle: `parent_ma.source()` returns `Option<ValueSource>`, and `Option::partial_cmp` puts `None < Some(_)`, meaning a parent with no source (impossible in practice, but defensively concerning) would always lose to any subcommand value.","fix":"Document the intended priority semantics explicitly. Add a debug assertion that both sources are `Some` at this point.","locations":[{"ref":"clap_builder/src/parser/arg_matcher.rs:66-75","code":" 66 | let to_update = if let Some(parent_ma) = vals_map.get(global_arg) {\n 67 | if parent_ma.source() > ma.source() {\n 68 | parent_ma\n 69 | } else {\n 70 | ma\n 71 | }\n 72 | } else {\n 73 | ma\n 74 | }\n 75 | .clone();","lines":10}]},{"id":"LOGIC_14","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `parse`, the local variable `trailing_values` is shadowed on line 301 with `let trailing_values = false;` inside the `ParseState::Opt(id)` branch. This means that when accumulating values for a pending option argument, the pending arg's `trailing_idx` is never set even when `trailing_values` (the outer) is `true`. Values received after `--` for a multi-value option will not be tagged as trailing, so `is_dont_delimit_trailing_values_set()` logic in `react` will not apply correctly to those values.","fix":"Remove the inner `let trailing_values = false;` and use the outer `trailing_values` variable so that trailing status is propagated to `pending_values_mut`.","locations":[{"ref":"clap_builder/src/parser/parser.rs:291-317","code":" 291 | if let ParseState::Opt(id) = &parse_state {\n 292 | // Assume this is a value of a previous arg.\n 293 | \n 294 | // get the option so we can check the settings\n 295 | let arg = &self.cmd[id];\n 296 | let parse_result = if let Some(parse_result) =\n 297 | self.check_terminator(arg, arg_os.to_value_os())\n 298 | {\n 299 | parse_result\n 300 | } else {\n 301 | let trailing_values = false;\n 302 | let arg_values = matcher.pending_values_mut(id, None, trailing_values);\n 303 | arg_values.push(arg_os.to_value_os().to_owned());\n 304 | if matcher.needs_more_vals(arg) {\n 305 | ParseResult::Opt(arg.get_id().clone())\n 306 | } else {\n 307 | ParseResult::ValuesDone\n 308 | }\n 309 | };\n 310 | parse_state = match parse_result {\n 311 | ParseResult::Opt(id) => ParseState::Opt(id),\n 312 | ParseResult::ValuesDone => ParseState::ValuesDone,\n 313 | _ => unreachable!(),\n 314 | };\n 315 | // get the next value from the iterator\n 316 | continue;\n 317 | }","lines":27},{"ref":"clap_builder/src/parser/parser.rs:301","code":" 301 | let trailing_values = false;","lines":1}]},{"id":"LOGIC_15","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `possible_subcommand` with `is_infer_subcommands_set()`, the function returns `Some(name)` only when exactly one subcommand prefix-matches. But when zero inference matches exist, it falls through to the exact `find_subcommand` lookup. This means that if inference is enabled and there are two subcommands `foo` and `foobar`, typing `foo` will fail to infer (ambiguous), but will still be found by exact match on the second lookup. The comment says 'don't use an else here because we want inference to support exact matching even if there are conflicts', which is correct behavior, but it means a user who types an ambiguous prefix will get the exact match silently without any 'ambiguous subcommand' warning, which is surprising.","fix":"This may be intentional, but it should be documented. Consider emitting a warning or hint about ambiguous inference when the exact match is used as a fallback.","locations":[{"ref":"clap_builder/src/parser/parser.rs:592-618","code":" 592 | if !(self.cmd.is_args_conflicts_with_subcommands_set() && valid_arg_found) {\n 593 | if self.cmd.is_infer_subcommands_set() {\n 594 | // For subcommand `test`, we accepts it's prefix: `t`, `te`,\n 595 | // `tes` and `test`.\n 596 | let mut iter = self.cmd.get_subcommands().filter_map(|s| {\n 597 | if s.get_name().starts_with(arg) {\n 598 | return Some(s.get_name());\n 599 | }\n 600 | \n 601 | // Use find here instead of chaining the iterator because we want to accept\n 602 | // conflicts in aliases.\n 603 | s.get_all_aliases().find(|s| s.starts_with(arg))\n 604 | });\n 605 | \n 606 | if let name @ Some(_) = iter.next() {\n 607 | if iter.next().is_none() {\n 608 | return name;\n 609 | }\n 610 | }\n 611 | }\n 612 | // Don't use an else here because we want inference to support exact matching even if\n 613 | // there are conflicts.\n 614 | if let Some(sc) = self.cmd.find_subcommand(arg) {\n 615 | return Some(sc.get_name());\n 616 | }\n 617 | }\n 618 | None","lines":27}]},{"id":"LOGIC_16","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `Parser::react`, for `ArgAction::Set` and `ArgAction::SetTrue`/`SetFalse`, when the argument is already present and both `is_args_override_self()` and `arg.overrides.contains(arg.get_id())` are false, an `ArgumentConflict` error is returned with `vec![arg.to_string()]` as the conflicting arg (i.e. the arg conflicts with itself). This is the intended behavior for 'no duplicates allowed', but the error message will display the argument as conflicting with itself, which is confusing to end users who see 'argument X cannot be used with X'.","fix":"In `format.rs` `write_dynamic_context`, the `ArgumentConflict` branch already handles the self-conflict case (line 154-159 in format.rs), showing 'cannot be used multiple times'. This is correct, so no code change is needed, but a comment explaining the self-conflict intent in `react` would clarify the design.","locations":[{"ref":"clap_builder/src/parser/parser.rs:1208-1217","code":" 1208 | if matcher.remove(arg.get_id())\n 1209 | && !(self.cmd.is_args_override_self() || arg.overrides.contains(arg.get_id()))\n 1210 | {\n 1211 | return Err(ClapError::argument_conflict(\n 1212 | self.cmd,\n 1213 | arg.to_string(),\n 1214 | vec![arg.to_string()],\n 1215 | Usage::new(self.cmd).create_usage_with_title(&[]),\n 1216 | ));\n 1217 | }","lines":10},{"ref":"clap_builder/src/parser/parser.rs:1251-1260","code":" 1251 | if matcher.remove(arg.get_id())\n 1252 | && !(self.cmd.is_args_override_self() || arg.overrides.contains(arg.get_id()))\n 1253 | {\n 1254 | return Err(ClapError::argument_conflict(\n 1255 | self.cmd,\n 1256 | arg.to_string(),\n 1257 | vec![arg.to_string()],\n 1258 | Usage::new(self.cmd).create_usage_with_title(&[]),\n 1259 | ));\n 1260 | }","lines":10},{"ref":"clap_builder/src/parser/parser.rs:1272-1281","code":" 1272 | if matcher.remove(arg.get_id())\n 1273 | && !(self.cmd.is_args_override_self() || arg.overrides.contains(arg.get_id()))\n 1274 | {\n 1275 | return Err(ClapError::argument_conflict(\n 1276 | self.cmd,\n 1277 | arg.to_string(),\n 1278 | vec![arg.to_string()],\n 1279 | Usage::new(self.cmd).create_usage_with_title(&[]),\n 1280 | ));\n 1281 | }","lines":10}]},{"id":"LOGIC_17","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `Parser::react` for `ArgAction::Count`, on line 1288-1290 the existing count is fetched with `matcher.get_one::<crate::builder::CountType>(...)`. `CountType` is `u8`. `saturating_add(1)` will silently cap at 255 rather than wrapping or returning an error. Repeated `--verbose` flags beyond 255 will appear to have the same count as 255, which is surprising but at least safe. However, the overflow is silent with no warning.","fix":"This is a documented `u8` type limit. Add a note in the public docs for `ArgAction::Count` clarifying the saturation behavior, or use a wider type or emit a warning when saturation occurs.","locations":[{"ref":"clap_builder/src/parser/parser.rs:1286-1295","code":" 1286 | ArgAction::Count => {\n 1287 | let raw_vals = if raw_vals.is_empty() {\n 1288 | let existing_value = *matcher\n 1289 | .get_one::<crate::builder::CountType>(arg.get_id().as_str())\n 1290 | .unwrap_or(&0);\n 1291 | let next_value = existing_value.saturating_add(1);\n 1292 | vec![OsString::from(next_value.to_string())]\n 1293 | } else {\n 1294 | raw_vals\n 1295 | };","lines":10}]},{"id":"LOGIC_18","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `Conflicts::gather_conflicts`, when both `arg_id_conflicts.contains(other_arg_id)` and `other_arg_id_conflicts.contains(arg_id)` are both true (a symmetric conflict), `other_arg_id` is pushed to `conflicts` twice. The resulting duplicate conflict IDs are later deduplicated via `FlatSet` in `build_conflict_err`, but this is wasteful and the deduplication is easy to miss - if a future code path iterates `conflicts` without deduplication, it would report the same conflict twice.","fix":"Use an early-continue or combine the two `contains` checks with `||` to avoid inserting duplicates; or add a `debug_assert` that the FlatSet deduplication is still applied.","locations":[{"ref":"clap_builder/src/parser/validator.rs:469-479","code":" 469 | for (other_arg_id, other_arg_id_conflicts) in self.potential.iter() {\n 470 | if arg_id == other_arg_id {\n 471 | continue;\n 472 | }\n 473 | \n 474 | if arg_id_conflicts.contains(other_arg_id) {\n 475 | conflicts.push(other_arg_id.clone());\n 476 | }\n 477 | if other_arg_id_conflicts.contains(arg_id) {\n 478 | conflicts.push(other_arg_id.clone());\n 479 | }","lines":11},{"ref":"clap_builder/src/parser/validator.rs:135-145","code":" 135 | let conflict_ids = conflict_ids\n 136 | .iter()\n 137 | .flat_map(|c_id| {\n 138 | if self.cmd.find_group(c_id).is_some() {\n 139 | self.cmd.unroll_args_in_group(c_id)\n 140 | } else {\n 141 | vec![c_id.clone()]\n 142 | }\n 143 | })\n 144 | .collect::<FlatSet<_>>()\n 145 | .into_vec();","lines":11}]},{"id":"LOGIC_19","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"The fish generator `value_completion` places possible value names inside a fish double-quoted string (the `-a \"...\"` argument at line 285-301). The `escape_string` call uses `escape_comma=true` but does not escape `\"` (double-quote). A possible value name containing a double-quote character will break the surrounding double-quoted string in the generated fish completion command.","fix":"Add escaping of `\"` (replace `\"` with `\\\"`) in `escape_string`, or switch the outer delimiter to single-quotes for the possible-values list.","locations":[{"ref":"clap_complete/src/aot/shells/fish.rs:283-301","code":" 283 | // We return the possible values with their own empty description e.g. \"a\\t''\\nb\\t''\"\n 284 | // this makes sure that a and b don't get the description of the option or argument\n 285 | format!(\n 286 | \" -r -f -a \\\"{}\\\"\",\n 287 | data.iter()\n 288 | .filter_map(|value| if value.is_hide_set() {\n 289 | None\n 290 | } else {\n 291 | // The help text after \\t is wrapped in '' to make sure that the it is taken literally\n 292 | // and there is no command substitution or variable expansion resulting in unexpected errors\n 293 | Some(format!(\n 294 | \"{}\\\\t'{}'\",\n 295 | escape_string(value.get_name(), true).as_str(),\n 296 | escape_help(value.get_help().unwrap_or_default())\n 297 | ))\n 298 | })\n 299 | .collect::<Vec<_>>()\n 300 | .join(\"\\n\")\n 301 | )","lines":19}]},{"id":"LOGIC_20","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"The PowerShell AOT generator's `escape_help` falls back to `data.to_string()` (line 76) when the help string is empty. In `generate_aliases`, `aliases[0]` (a `char`) is passed as `data`, so the fallback is `char::to_string()`. While this is harmless for the char itself, if the help string is non-empty and contains `'` it is correctly escaped; however, if `bin_name` or any subcommand name passed to `escape_help` contains `'`, the fallback path skips escaping entirely, allowing a single-quote to break the outer single-quoted PS string.","fix":"Always apply `escape_string` to `data.to_string()` in the fallback path, i.e. `escape_string(&data.to_string())` instead of `data.to_string()`.","locations":[{"ref":"clap_complete/src/aot/shells/powershell.rs:69-77","code":" 69 | fn escape_help<T: ToString>(help: Option<&StyledStr>, data: T) -> String {\n 70 | if let Some(help) = help {\n 71 | let help_str = help.to_string();\n 72 | if !help_str.is_empty() {\n 73 | return escape_string(&help_str.replace('\\n', \" \"));\n 74 | }\n 75 | }\n 76 | data.to_string()\n 77 | }","lines":9}]},{"id":"LOGIC_21","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"The PowerShell AOT generator inserts option name strings (long and short option names) directly into PS single-quoted strings without checking for single-quotes in the name (lines 140, 151). If an option's long name contains `'`, the generated PS completions will have a syntax error.","fix":"Pass option long and short names through `escape_string` before embedding them in the single-quoted PS strings.","locations":[{"ref":"clap_complete/src/aot/shells/powershell.rs:132-154","code":" 132 | fn generate_aliases(completions: &mut String, preamble: &String, arg: &Arg) {\n 133 | use std::fmt::Write as _;\n 134 | \n 135 | if let Some(aliases) = arg.get_short_and_visible_aliases() {\n 136 | let tooltip = escape_help(arg.get_help(), aliases[0]);\n 137 | for alias in aliases {\n 138 | let _ = write!(\n 139 | completions,\n 140 | \"{preamble}'-{alias}', '-{alias}{}', [CompletionResultType]::ParameterName, '{tooltip}')\",\n 141 | // make PowerShell realize there is a difference between `-s` and `-S`\n 142 | if alias.is_uppercase() { \" \" } else { \"\" },\n 143 | );\n 144 | }\n 145 | }\n 146 | if let Some(aliases) = arg.get_long_and_visible_aliases() {\n 147 | let tooltip = escape_help(arg.get_help(), aliases[0]);\n 148 | for alias in aliases {\n 149 | let _ = write!(\n 150 | completions,\n 151 | \"{preamble}'--{alias}', '--{alias}', [CompletionResultType]::ParameterName, '{tooltip}')\"\n 152 | );\n 153 | }\n 154 | }","lines":23}]},{"id":"LOGIC_22","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `engine/complete.rs`, the `complete_option` function only looks up long options by exact match of the `get_long()` field (line 286: `a.get_long() == Some(flag)`), ignoring long visible aliases when completing `--flag=value`. If a user types `--alias=value` where `alias` is a visible long alias (not the canonical long name), no value completions are provided, even though the alias is valid.","fix":"Use `a.get_long_and_visible_aliases().map(|v| v.contains(&flag)).unwrap_or(false)` instead of `a.get_long() == Some(flag)` to include all visible long aliases when looking up the argument.","locations":[{"ref":"clap_complete/src/engine/complete.rs:283-304","code":" 283 | } else if let Some((flag, value)) = arg.to_long() {\n 284 | if let Ok(flag) = flag {\n 285 | if let Some(value) = value {\n 286 | if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) {\n 287 | completions.extend(\n 288 | complete_arg_value(value.to_str().ok_or(value), arg, current_dir, 0)\n 289 | .into_iter()\n 290 | .map(|comp| comp.add_prefix(format!(\"--{flag}=\"))),\n 291 | );\n 292 | }\n 293 | } else {\n 294 | completions.extend(\n 295 | longs_and_visible_aliases(cmd)\n 296 | .into_iter()\n 297 | .filter(|comp| comp.get_value().starts_with(format!(\"--{flag}\").as_str())),\n 298 | );\n 299 | completions.extend(\n 300 | hidden_longs_aliases(cmd)\n 301 | .into_iter()\n 302 | .filter(|comp| comp.get_value().starts_with(format!(\"--{flag}\").as_str())),\n 303 | );\n 304 | }","lines":22}]},{"id":"LOGIC_23","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In the nushell generator `append_value_completion_defs`, the call to `arg.get_possible_values()` at line 114 returns all possible values, including hidden ones. Hidden values (those with `is_hide_set() == true`) are then emitted into the Nushell completion list, exposing internal or undocumented values to users. All other shell generators filter out hidden values.","fix":"Filter out hidden values: `arg.get_possible_values().iter().filter(|v| !v.is_hide_set())` before iterating.","locations":[{"ref":"clap_complete_nushell/src/lib.rs:113-131","code":" 113 | fn append_value_completion_defs(arg: &Arg, name: &str, s: &mut String) {\n 114 | let possible_values = arg.get_possible_values();\n 115 | if possible_values.is_empty() {\n 116 | return;\n 117 | }\n 118 | \n 119 | s.push_str(format!(r#\" def \"nu-complete {} {}\" [] {{\"#, name, arg.get_id()).as_str());\n 120 | s.push_str(\"\\n [\");\n 121 | \n 122 | for value in possible_values {\n 123 | let vname = value.get_name();\n 124 | if vname.contains(|c: char| c.is_whitespace()) {\n 125 | s.push_str(format!(r#\" \"\\\"{vname}\\\"\"\"#).as_str());\n 126 | } else {\n 127 | s.push_str(format!(r#\" \"{vname}\"\"#).as_str());\n 128 | }\n 129 | }\n 130 | \n 131 | s.push_str(\" ]\\n }\\n\\n\");","lines":19}]},{"id":"LOGIC_24","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In the nushell generator `generate_completion`, the top-level command name is emitted unquoted in `export extern {name} [` at line 219. If the bin name contains spaces (e.g. `my tool`), the generated Nushell script is syntactically invalid: `export extern my tool [` is not valid Nushell. Subcommand names are correctly double-quoted at line 217.","fix":"Always use the quoted form `export extern \"{name}\" [` for both the top-level command and subcommands, removing the distinction between `is_subcommand` branches for the extern declaration.","locations":[{"ref":"clap_complete_nushell/src/lib.rs:216-219","code":" 216 | if is_subcommand {\n 217 | completions.push_str(format!(\" export extern \\\"{name}\\\" [\\n\").as_str());\n 218 | } else {\n 219 | completions.push_str(format!(\" export extern {name} [\\n\").as_str());","lines":4}]},{"id":"LOGIC_25","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"In `clap_mangen/src/render.rs`, the `subcommands` function emits a subcommand entry using `roff.text([roman(name)])` where `name` is a plain Rust `String` built at line 222-227. The `name` string (e.g. `myapp-subcommand(1)`) and the help lines at line 230-233 are passed to `roman()` which emits them as roff roman text. However the `about` string is taken verbatim from user-provided help text with `roff.text([roman(line)])` - roff inline text does not escape backslashes or periods at the start of lines, so a help text line starting with `.` or `'` is interpreted as a roff control line, corrupting the man page.","fix":"The roff crate should ideally handle this, but if it does not escape line-initial `.`/`'` and backslashes, wrap such lines or prepend `\\&` to neutralize them. Verify and add a test for help text containing roff-special characters.","locations":[{"ref":"clap_mangen/src/render.rs:215-236","code":" 215 | pub(crate) fn subcommands(roff: &mut Roff, cmd: &clap::Command, section: &str) {\n 216 | let mut sorted_subcommands: Vec<_> =\n 217 | cmd.get_subcommands().filter(|s| !s.is_hide_set()).collect();\n 218 | sorted_subcommands.sort_by_key(|c| subcommand_sort_key(c));\n 219 | for sub in sorted_subcommands {\n 220 | roff.control(\"TP\", []);\n 221 | \n 222 | let name = format!(\n 223 | \"{}-{}({})\",\n 224 | cmd.get_display_name().unwrap_or_else(|| cmd.get_name()),\n 225 | sub.get_name(),\n 226 | section\n 227 | );\n 228 | roff.text([roman(name)]);\n 229 | \n 230 | if let Some(about) = sub.get_about().or_else(|| sub.get_long_about()) {\n 231 | for line in about.to_string().lines() {\n 232 | roff.text([roman(line)]);\n 233 | }\n 234 | }\n 235 | }\n 236 | }","lines":22}]},{"id":"LOGIC_26","type":"logic_error","type_label":"Logic Error","severity":"low","easy":true,"description":"`Arg::raw()` documentation states that the method implies `ArgAction::Set` when the user does not\nexplicitly set an action, but the implementation only sets `num_vals`, `allow_hyphen_values`, and\n`last`. It never assigns `self.action = Some(ArgAction::Set)`. The actual action is therefore\nwhatever was previously set (or the inferred default from `_build()`), which may not be\n`ArgAction::Set`. If a user relies on the documented guarantee, their argument may behave\ndifferently than expected.","fix":"Either add `self.action.get_or_insert(ArgAction::Set)` in the `raw()` body to make the\nimplementation match the documentation, or remove the claim from the doc comment.","locations":[{"ref":"clap_builder/src/builder/arg.rs:1778-1783","code":" 1778 | pub fn raw(mut self, yes: bool) -> Self {\n 1779 | if yes {\n 1780 | self.num_vals.get_or_insert_with(|| (1..).into());\n 1781 | }\n 1782 | self.allow_hyphen_values(yes).last(yes)\n 1783 | }","lines":6}]},{"id":"LOGIC_27","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In debug_asserts.rs, the 'allow_missing_positional' branch of _verify_positionals iterates positionals in forward (unspecified) order and tracks 'found' / 'foundx2' state to detect required-before-optional violations. The iteration order is not guaranteed to be by index (it depends on insertion order in get_positionals()), so the check may produce false negatives or false positives if positionals are not yielded in index order.","fix":"Sort positionals by index before iterating in the allow_missing_positional branch, the same way the else branch iterates explicitly by index with (1..=num_p).rev().","locations":[{"ref":"clap_builder/src/builder/debug_asserts.rs:621-651","code":" 621 | if cmd.is_allow_missing_positional_set() {\n 622 | // Check that if a required positional argument is found, all positions with a lower\n 623 | // index are also required.\n 624 | let mut foundx2 = false;\n 625 | \n 626 | for p in cmd.get_positionals() {\n 627 | if foundx2 && !p.is_required_set() {\n 628 | assert!(\n 629 | p.is_required_set(),\n 630 | \"Found non-required positional argument with a lower \\\n 631 | index than a required positional argument by two or more: {:?} \\\n 632 | index {:?}\",\n 633 | p.get_id(),\n 634 | p.get_index()\n 635 | );\n 636 | } else if p.is_required_set() && !p.is_last_set() {\n 637 | // Args that .last(true) don't count since they can be required and have\n 638 | // positionals with a lower index that aren't required\n 639 | // Imagine: prog <req1> [opt1] -- <req2>\n 640 | // Both of these are valid invocations:\n 641 | // $ prog r1 -- r2\n 642 | // $ prog r1 o1 -- r2\n 643 | if found {\n 644 | foundx2 = true;\n 645 | continue;\n 646 | }\n 647 | found = true;\n 648 | } else {\n 649 | found = false;\n 650 | }\n 651 | }","lines":31}]},{"id":"LOGIC_28","type":"logic_error","type_label":"Logic Error","severity":"low","easy":true,"description":"`LineWrapper::wrap` (wrap_algorithms.rs line 58) tracks line width by adding\n`indentation.len()` (byte count) when re-indenting after a line break, but\nthe rest of the wrapping logic tracks width using `display_width` (visual\ncolumn count). For ASCII-only indentation (spaces) these are the same, but\nif indentation ever contains multi-byte characters the byte count and display\nwidth diverge, causing incorrect wrapping decisions on subsequent words in\nre-indented lines. The same function calls `display_width(trimmed)` for word\nwidths, making the inconsistency visible.","fix":"Replace `self.line_width += indentation.len()` with\n`self.line_width += display_width(indentation)` to maintain consistent\nvisual-column accounting throughout the wrapping loop.","locations":[{"ref":"clap_builder/src/output/textwrap/wrap_algorithms.rs:56-59","code":" 56 | if let Some(indentation) = self.indentation {\n 57 | words.insert(i, indentation);\n 58 | self.line_width += indentation.len();\n 59 | i += 1;","lines":4}]},{"id":"LOGIC_29","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In `Parser::parse`, `pos_counter` starts at 1. When all positional arguments have been consumed and a value is received with no matching positional, the code reaches the external-subcommand or error path. However, if `pos_counter` overflows `positional_count` due to increments in the loop (line 429, 417), the `get_keymap().get(&pos_counter)` lookup returns `None` and the code falls through to the external subcommand or error path. This is correct behavior, but there is no assertion that `pos_counter` stays within a bounded range, and a very large number of positional arguments could cause issues.","fix":"Add a debug assertion that `pos_counter` does not exceed `positional_count + 1` in normal operation.","locations":[{"ref":"clap_builder/src/parser/parser.rs:392-481","code":" 392 | if let Some(arg) = self.cmd.get_keymap().get(&pos_counter) {\n 393 | if arg.is_last_set() && !trailing_values {\n 394 | let _ = self.resolve_pending(matcher);\n 395 | // Its already considered a positional, we don't need to suggest turning it\n 396 | // into one\n 397 | let suggested_trailing_arg = false;\n 398 | return Err(ClapError::unknown_argument(\n 399 | self.cmd,\n 400 | arg_os.display().to_string(),\n 401 | None,\n 402 | suggested_trailing_arg,\n 403 | Usage::new(self.cmd).create_usage_with_title(&[]),\n 404 | ));\n 405 | }\n 406 | \n 407 | if arg.is_trailing_var_arg_set() {\n 408 | trailing_values = true;\n 409 | }\n 410 | \n 411 | if matcher.pending_arg_id() != Some(arg.get_id()) || !arg.is_multiple_values_set() {\n 412 | ok!(self.resolve_pending(matcher));\n 413 | }\n 414 | parse_state =\n 415 | if let Some(parse_result) = self.check_terminator(arg, arg_os.to_value_os()) {\n 416 | debug_assert_eq!(parse_result, ParseResult::ValuesDone);\n 417 | pos_counter += 1;\n 418 | ParseState::ValuesDone\n 419 | } else {\n 420 | let arg_values = matcher.pending_values_mut(\n 421 | arg.get_id(),\n 422 | Some(Identifier::Index),\n 423 | trailing_values,\n 424 | );\n 425 | arg_values.push(arg_os.to_value_os().to_owned());\n 426 | \n 427 | // Only increment the positional counter if it doesn't allow multiples\n 428 | if !arg.is_multiple() {\n 429 | pos_counter += 1;\n 430 | ParseState::ValuesDone\n 431 | } else {\n 432 | ParseState::Pos(arg.get_id().clone())\n 433 | }\n 434 | };\n 435 | valid_arg_found = true;\n 436 | } else if let Some(external_parser) =\n 437 | self.cmd.get_external_subcommand_value_parser().cloned()\n 438 | {\n 439 | // Get external subcommand name\n 440 | let sc_name = match arg_os.to_value() {\n 441 | Ok(s) => s.to_owned(),\n 442 | Err(_) => {\n 443 | let _ = self.resolve_pending(matcher);\n 444 | return Err(ClapError::invalid_utf8(\n 445 | self.cmd,\n 446 | Usage::new(self.cmd).create_usage_with_title(&[]),\n 447 | ));\n 448 | }\n 449 | };\n 450 | \n 451 | // Collect the external subcommand args\n 452 | let mut sc_m = ArgMatcher::new(self.cmd);\n 453 | sc_m.start_occurrence_of_external(self.cmd);\n 454 | \n 455 | for raw_val in raw_args.remaining(&mut args_cursor) {\n 456 | let val = ok!(external_parser.parse_ref(\n 457 | self.cmd,\n 458 | None,\n 459 | raw_val,\n 460 | ValueSource::CommandLine\n 461 | ));\n 462 | let external_id = Id::from_static_ref(Id::EXTERNAL);\n 463 | sc_m.add_val_to(&external_id, val, raw_val.to_os_string());\n 464 | }\n 465 | \n 466 | matcher.subcommand(SubCommand {\n 467 | name: sc_name,\n 468 | matches: sc_m.into_inner(),\n 469 | });\n 470 | \n 471 | return Ok(());\n 472 | } else {\n 473 | // Start error processing\n 474 | let _ = self.resolve_pending(matcher);\n 475 | return Err(self.match_arg_error(\n 476 | &arg_os,\n 477 | valid_arg_found,\n 478 | trailing_values,\n 479 | matcher,\n 480 | ));\n 481 | }","lines":90},{"ref":"clap_builder/src/parser/parser.rs:417-429","code":" 417 | pos_counter += 1;\n 418 | ParseState::ValuesDone\n 419 | } else {\n 420 | let arg_values = matcher.pending_values_mut(\n 421 | arg.get_id(),\n 422 | Some(Identifier::Index),\n 423 | trailing_values,\n 424 | );\n 425 | arg_values.push(arg_os.to_value_os().to_owned());\n 426 | \n 427 | // Only increment the positional counter if it doesn't allow multiples\n 428 | if !arg.is_multiple() {\n 429 | pos_counter += 1;","lines":13}]},{"id":"LOGIC_30","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In `build_conflict_err_usage` in `validator.rs`, the `required` field is built by chaining `arg.requires` and then `used_filtered` (lines 183-190). This can include duplicate IDs if an arg appears in both `used_filtered` and as a requirement. The `create_usage_with_title` function downstream may or may not deduplicate, leading to duplicate entries in the displayed usage string.","fix":"Deduplicate the `required` vec before passing it to `create_usage_with_title`.","locations":[{"ref":"clap_builder/src/parser/validator.rs:183-193","code":" 183 | let required: Vec<Id> = used_filtered\n 184 | .iter()\n 185 | .filter_map(|key| self.cmd.find(key))\n 186 | .flat_map(|arg| arg.requires.iter().map(|item| &item.1))\n 187 | .filter(|key| !used_filtered.contains(key) && !conflicting_keys.contains(key))\n 188 | .chain(used_filtered.iter())\n 189 | .cloned()\n 190 | .collect();\n 191 | Usage::new(self.cmd)\n 192 | .required(&self.required)\n 193 | .create_usage_with_title(&required)","lines":11}]},{"id":"LOGIC_31","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In `validate_required`, the 'missing preceding positionals for display' loop (lines 322-336 in validator.rs) adds positional arguments whose index is less than `highest_index` to `missing_required`, even if those positional args are themselves not actually required (e.g. they are optional positionals without a `required()` call). This can result in optional positional arguments being listed as 'missing required' in error messages purely because they precede a truly-required positional that has a higher index.","fix":"Filter the preceding positionals to only those that are actually required before adding them to `missing_required`.","locations":[{"ref":"clap_builder/src/parser/validator.rs:321-336","code":" 321 | // For display purposes, include all of the preceding positional arguments\n 322 | if !self.cmd.is_allow_missing_positional_set() {\n 323 | for pos in self\n 324 | .cmd\n 325 | .get_positionals()\n 326 | .filter(|a| !matcher.check_explicit(a.get_id(), &ArgPredicate::IsPresent))\n 327 | {\n 328 | if pos.get_index() < Some(highest_index) {\n 329 | debug!(\n 330 | \"Validator::validate_required:iter: Missing {:?}\",\n 331 | pos.get_id()\n 332 | );\n 333 | missing_required.push(pos.get_id().clone());\n 334 | }\n 335 | }\n 336 | }","lines":16}]},{"id":"LOGIC_32","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In the fish AOT generator's `gen_fish_inner`, subcommand names are embedded in the `-n` condition string using `format!(\" {name}\")` at line 113 without any escaping. If a subcommand name contains a fish special character (e.g. single-quote or space), the generated `__fish_seen_subcommand_from` call becomes syntactically malformed.","fix":"Apply `escape_string(name, false)` when embedding subcommand names into the `-n` condition string.","locations":[{"ref":"clap_complete/src/aot/shells/fish.rs:104-115","code":" 104 | [command] => {\n 105 | out.push_str(&format!(\" {command}\"));\n 106 | if cmd.has_subcommands() {\n 107 | out.push_str(\"; and not __fish_seen_subcommand_from\");\n 108 | }\n 109 | let subcommands = cmd\n 110 | .get_subcommands()\n 111 | .flat_map(Command::get_name_and_visible_aliases);\n 112 | for name in subcommands {\n 113 | out.push_str(&format!(\" {name}\"));\n 114 | }\n 115 | }","lines":12}]},{"id":"LOGIC_33","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In `clap_complete/src/env/mod.rs`, `try_complete_` calls `args.remove(0)` at line 235 to extract the completer binary path. If `try_complete` is called with an empty iterator (e.g. via the public `try_complete` API), this panics with an index-out-of-bounds. The `COMPLETE` env var check at line 215 cannot prevent an empty args list from reaching this point.","fix":"Guard with `if args.is_empty() { return Err(...) }` before calling `args.remove(0)`, or return an appropriate error.","locations":[{"ref":"clap_complete/src/env/mod.rs:235","code":" 235 | let completer = args.remove(0);","lines":1}]},{"id":"LOGIC_34","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"In `clap_mangen/src/render.rs`, `option_default_values` joins default values with `,` (a bare comma, line 342) without quoting values that may themselves contain commas. A default value like `a,b` would produce `[default: a,b]` which is ambiguous - readers cannot tell if it's one value `a,b` or two values `a` and `b`.","fix":"Join with `\", \"` (comma-space) instead of bare `,`, or quote each value individually, to make the formatting unambiguous.","locations":[{"ref":"clap_mangen/src/render.rs:332-347","code":" 332 | fn option_default_values(opt: &Arg) -> Option<String> {\n 333 | if opt.is_hide_default_value_set() || !opt.get_num_args().expect(\"built\").takes_values() {\n 334 | return None;\n 335 | } else if !opt.get_default_values().is_empty() {\n 336 | let values = opt\n 337 | .get_default_values()\n 338 | .iter()\n 339 | .map(|s| s.to_string_lossy())\n 340 | .collect::<Vec<_>>()\n 341 | .join(\",\");\n 342 | \n 343 | return Some(format!(\"[default: {values}]\"));\n 344 | }\n 345 | \n 346 | None\n 347 | }","lines":16}]},{"id":"CPY_1","type":"copy_paste","type_label":"Copy Paste","severity":"low","easy":true,"description":"In debug_asserts.rs, the error message for the r_unless_all loop uses 'required_unless*' (identical to the r_unless message). The message should refer to 'required_unless_all' to help users distinguish which constraint triggered the error.","fix":"Change the assert message from \"Argument {}: `required` conflicts with `required_unless*`\" to \"Argument {}: `required` conflicts with `required_unless_all`\".","locations":[{"ref":"clap_builder/src/builder/debug_asserts.rs:217-221","code":" 217 | assert!(\n 218 | !arg.is_required_set(),\n 219 | \"Argument {}: `required` conflicts with `required_unless*`\",\n 220 | arg.get_id()\n 221 | );","lines":5}]},{"id":"CPY_2","type":"copy_paste","type_label":"Copy Paste","severity":"low","easy":true,"description":"In debug_asserts.rs, the second assert inside the r_unless_all loop uses the message 'required_unless*' rather than 'required_unless_all'. This identical message appears in both the r_unless and r_unless_all loops, making it impossible to distinguish which loop fired.","fix":"Change the message from \"specified in 'required_unless*'\" to \"specified in 'required_unless_all'\" for the r_unless_all loop.","locations":[{"ref":"clap_builder/src/builder/debug_asserts.rs:222-229","code":" 222 | assert!(\n 223 | cmd.id_exists(req),\n 224 | \"Command {}: Argument or group '{}' specified in 'required_unless*' for '{}' does not exist\",\n 225 | cmd.get_name(),\n 226 | req,\n 227 | arg.get_id(),\n 228 | );\n 229 | }","lines":8}]},{"id":"CPY_3","type":"copy_paste","type_label":"Copy Paste","severity":"low","easy":true,"description":"In value_parser.rs, UnknownArgumentValueParser::parse_ref_ uses the string literal \"..\" (two dots) as the fallback argument label when arg is None, while every other parser in the same file consistently uses \"...\" (three dots). This inconsistency makes error messages for external-subcommand parsing look different from all other error messages.","fix":"Change `None => \"..\"` to `None => \"...\"`.","locations":[{"ref":"clap_builder/src/builder/value_parser.rs:2214","code":" 2214 | None => \"..\".to_owned(),","lines":1}]},{"id":"ERR_1","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","easy":false,"description":"In `get_matches_with`, the `inspect_err` closure at line 59-65 discards error results from `add_env` and `add_defaults` with `let _ =`. If either of those produces an error (e.g. a value parser fails on a default or env value), the error is silently lost and parsing continues as if everything is fine. This can lead to `ArgMatches` being in a partially-initialized state.","fix":"Store errors from `add_env`/`add_defaults` in a local variable and propagate them, or at minimum log them in debug builds.","locations":[{"ref":"clap_builder/src/parser/parser.rs:59-65","code":" 59 | .inspect_err(|_err| {\n 60 | if self.cmd.is_ignore_errors_set() {\n 61 | #[cfg(feature = \"env\")]\n 62 | let _ = self.add_env(matcher);\n 63 | let _ = self.add_defaults(matcher);\n 64 | }\n 65 | }));","lines":7}]},{"id":"API_1","type":"api_misuse","type_label":"Api Misuse","severity":"low","easy":true,"description":"ArgGroup::is_multiple takes &mut self even though it does not mutate the receiver. This forces callers to hold an exclusive borrow unnecessarily, which can prevent other immutable borrows from coexisting. Public API with needless &mut is a footgun.","fix":"Change the signature from `pub fn is_multiple(&mut self) -> bool` to `pub fn is_multiple(&self) -> bool`. No other change is needed.","locations":[{"ref":"clap_builder/src/builder/arg_group.rs:263-265","code":" 263 | pub fn is_multiple(&mut self) -> bool {\n 264 | self.multiple\n 265 | }","lines":3}]},{"id":"API_2","type":"api_misuse","type_label":"Api Misuse","severity":"low","easy":true,"description":"`Command::long_flag()` takes `impl Into<Str>` rather than `impl IntoResettable<Str>` like every\nother similar setter in the same struct (e.g. `alias`, `name`, `bin_name`). This means there is no\nway to clear a previously set `long_flag` through the builder API, which breaks the composability\npattern used everywhere else in the builder and makes it impossible to unset `long_flag` in\n`mut_subcommand` or similar contexts.","fix":"Change the signature to `pub fn long_flag(mut self, long: impl IntoResettable<Str>) -> Self` and\nhandle the `None` case by setting `self.long_flag = None`.","locations":[{"ref":"clap_builder/src/builder/command.rs:2650-2654","code":" 2650 | #[must_use]\n 2651 | pub fn long_flag(mut self, long: impl Into<Str>) -> Self {\n 2652 | self.long_flag = Some(long.into());\n 2653 | self\n 2654 | }","lines":5}]},{"id":"PERF_1","type":"performance","type_label":"Performance","severity":"medium","easy":false,"description":"In `did_you_mean_flag`, for each candidate subcommand in the suggestions fallback branch (line 66-84 in suggestions.rs), `subcommand._build_self(false)` is called, which triggers lazy initialization of the subcommand's keymap. This is called every time an unrecognized long argument is used, which iterates and builds ALL subcommands. This could be expensive for CLIs with many subcommands, since subcommand building is not cheap.","fix":"Cache whether subcommands have been built, or only build subcommands lazily in the error path. Alternatively, only build the subcommand if a fuzzy-matching flag candidate is found in that subcommand.","locations":[{"ref":"clap_builder/src/parser/features/suggestions.rs:65-85","code":" 65 | .into_iter()\n 66 | .filter_map(|subcommand| {\n 67 | subcommand._build_self(false);\n 68 | \n 69 | let longs = subcommand.get_keymap().keys().filter_map(|a| {\n 70 | if let KeyType::Long(v) = a {\n 71 | Some(v.to_string_lossy().into_owned())\n 72 | } else {\n 73 | None\n 74 | }\n 75 | });\n 76 | \n 77 | let subcommand_name = subcommand.get_name();\n 78 | \n 79 | let candidate = some!(did_you_mean(arg, longs).pop());\n 80 | let score = some!(remaining_args.iter().position(|x| subcommand_name == *x));\n 81 | Some((score, (candidate, Some(subcommand_name.to_owned()))))\n 82 | })\n 83 | .min_by_key(|(x, _)| *x)\n 84 | .map(|(_, suggestion)| suggestion),\n 85 | }","lines":21},{"ref":"clap_builder/src/parser/parser.rs:1575-1580","code":" 1575 | let did_you_mean = suggestions::did_you_mean_flag(\n 1576 | arg,\n 1577 | remaining_args,\n 1578 | longs.iter().map(|x| &x[..]),\n 1579 | self.cmd.get_subcommands_mut(),\n 1580 | );","lines":6}]},{"id":"PERF_2","type":"performance","type_label":"Performance","severity":"medium","easy":false,"description":"In `FlatMap::insert`, a linear scan through all keys is performed to check for an existing key before insertion (O(n)). Similarly, `FlatMap::get`, `get_mut`, `contains_key`, `entry`, and `remove_entry` all do O(n) scans. This is used for the `args` map in `ArgMatches` (keyed by `Id`), which is iterated on every access for every argument. For commands with many arguments (50+), common operations like `check_explicit` called in tight loops in the validator become O(n^2).","fix":"For larger argument counts, consider switching to a `HashMap`/`IndexMap` or adding a sorted-binary-search variant. At minimum, document the O(n) nature as a known trade-off for small n.","locations":[{"ref":"clap_builder/src/util/flat_map.rs:19-29","code":" 19 | pub(crate) fn insert(&mut self, key: K, mut value: V) -> Option<V> {\n 20 | for (index, existing) in self.keys.iter().enumerate() {\n 21 | if *existing == key {\n 22 | std::mem::swap(&mut self.values[index], &mut value);\n 23 | return Some(value);\n 24 | }\n 25 | }\n 26 | \n 27 | self.insert_unchecked(key, value);\n 28 | None\n 29 | }","lines":11},{"ref":"clap_builder/src/util/flat_map.rs:92-103","code":" 92 | pub(crate) fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>\n 93 | where\n 94 | K: Borrow<Q>,\n 95 | Q: Eq,\n 96 | {\n 97 | for (index, existing) in self.keys.iter().enumerate() {\n 98 | if existing.borrow() == k {\n 99 | return Some(&self.values[index]);\n 100 | }\n 101 | }\n 102 | None\n 103 | }","lines":12},{"ref":"clap_builder/src/parser/validator.rs:239-272","code":" 239 | for arg_or_group in self\n 240 | .required\n 241 | .iter()\n 242 | .filter(|r| !matcher.check_explicit(r, &ArgPredicate::IsPresent))\n 243 | {\n 244 | debug!(\"Validator::validate_required:iter:aog={arg_or_group:?}\");\n 245 | if let Some(arg) = self.cmd.find(arg_or_group) {\n 246 | debug!(\"Validator::validate_required:iter: This is an arg\");\n 247 | if !is_exclusive_present && !self.is_missing_required_ok(arg, conflicts) {\n 248 | debug!(\n 249 | \"Validator::validate_required:iter: Missing {:?}\",\n 250 | arg.get_id()\n 251 | );\n 252 | missing_required.push(arg.get_id().clone());\n 253 | if !arg.is_last_set() {\n 254 | highest_index = highest_index.max(arg.get_index().unwrap_or(0));\n 255 | }\n 256 | }\n 257 | } else if let Some(group) = self.cmd.find_group(arg_or_group) {\n 258 | debug!(\"Validator::validate_required:iter: This is a group\");\n 259 | if !self\n 260 | .cmd\n 261 | .unroll_args_in_group(&group.id)\n 262 | .iter()\n 263 | .any(|a| matcher.check_explicit(a, &ArgPredicate::IsPresent))\n 264 | {\n 265 | debug!(\n 266 | \"Validator::validate_required:iter: Missing {:?}\",\n 267 | group.get_id()\n 268 | );\n 269 | missing_required.push(group.get_id().clone());\n 270 | }\n 271 | }\n 272 | }","lines":34}]},{"id":"PERF_3","type":"performance","type_label":"Performance","severity":"low","easy":false,"description":"In debug_asserts.rs, the is_required_set() check inside the r_ifs, r_ifs_all, r_unless, and r_unless_all loops is evaluated once per entry in each collection, even though the result is constant across all iterations for a given argument. For arguments with many conditional requirements this is wasteful and could be checked once before the loop.","fix":"Move the is_required_set() assertion outside (before) each loop, e.g. assert!(!arg.is_required_set(), ...) once before iterating r_ifs, then iterate only to check cmd.id_exists.","locations":[{"ref":"clap_builder/src/builder/debug_asserts.rs:171-184","code":" 171 | for req in &arg.r_ifs {\n 172 | assert!(\n 173 | !arg.is_required_set(),\n 174 | \"Argument {}: `required` conflicts with `required_if_eq*`\",\n 175 | arg.get_id()\n 176 | );\n 177 | assert!(\n 178 | cmd.id_exists(&req.0),\n 179 | \"Command {}: Argument or group '{}' specified in 'required_if_eq*' for '{}' does not exist\",\n 180 | cmd.get_name(),\n 181 | req.0,\n 182 | arg.get_id()\n 183 | );\n 184 | }","lines":14},{"ref":"clap_builder/src/builder/debug_asserts.rs:186-199","code":" 186 | for req in &arg.r_ifs_all {\n 187 | assert!(\n 188 | !arg.is_required_set(),\n 189 | \"Argument {}: `required` conflicts with `required_if_eq_all`\",\n 190 | arg.get_id()\n 191 | );\n 192 | assert!(\n 193 | cmd.id_exists(&req.0),\n 194 | \"Command {}: Argument or group '{}' specified in 'required_if_eq_all' for '{}' does not exist\",\n 195 | cmd.get_name(),\n 196 | req.0,\n 197 | arg.get_id()\n 198 | );\n 199 | }","lines":14},{"ref":"clap_builder/src/builder/debug_asserts.rs:201-214","code":" 201 | for req in &arg.r_unless {\n 202 | assert!(\n 203 | !arg.is_required_set(),\n 204 | \"Argument {}: `required` conflicts with `required_unless*`\",\n 205 | arg.get_id()\n 206 | );\n 207 | assert!(\n 208 | cmd.id_exists(req),\n 209 | \"Command {}: Argument or group '{}' specified in 'required_unless*' for '{}' does not exist\",\n 210 | cmd.get_name(),\n 211 | req,\n 212 | arg.get_id(),\n 213 | );\n 214 | }","lines":14},{"ref":"clap_builder/src/builder/debug_asserts.rs:216-229","code":" 216 | for req in &arg.r_unless_all {\n 217 | assert!(\n 218 | !arg.is_required_set(),\n 219 | \"Argument {}: `required` conflicts with `required_unless*`\",\n 220 | arg.get_id()\n 221 | );\n 222 | assert!(\n 223 | cmd.id_exists(req),\n 224 | \"Command {}: Argument or group '{}' specified in 'required_unless*' for '{}' does not exist\",\n 225 | cmd.get_name(),\n 226 | req,\n 227 | arg.get_id(),\n 228 | );\n 229 | }","lines":14}]},{"id":"PERF_4","type":"performance","type_label":"Performance","severity":"low","easy":false,"description":"StyledStr::trim_start_lines only removes a single leading blank line (the first line). If help text starts with multiple blank lines, only the first is removed and the rest remain, causing extra blank lines at the start of help output. The plural name 'trim_start_lines' implies multiple lines should be stripped.","fix":"Replace the single-pass logic with a loop or use a while let pattern to repeatedly strip leading blank lines until a non-blank line is found at the start.","locations":[{"ref":"clap_builder/src/builder/styled_str.rs:50-57","code":" 50 | pub(crate) fn trim_start_lines(&mut self) {\n 51 | if let Some(pos) = self.0.find('\\n') {\n 52 | let (leading, help) = self.0.split_at(pos + 1);\n 53 | if leading.trim().is_empty() {\n 54 | self.0 = help.to_owned();\n 55 | }\n 56 | }\n 57 | }","lines":8}]},{"id":"PERF_5","type":"performance","type_label":"Performance","severity":"low","easy":false,"description":"`write_all_args` (help_template.rs lines 462-466) calls `self.cmd.clone()`\nfollowed by `cmd.build()` to enumerate flattened subcommands. `Command::build`\nis an expensive operation that resolves all arguments, propagates settings, and\ndeduplicates. Additionally, `write_help_usage` (usage.rs lines 114-115)\nperforms the same clone+build pattern. When help is rendered for a command\nwith many subcommands these clones and rebuilds occur on the hot path and\naccount for significant redundant work.","fix":"Store the pre-built command on `HelpTemplate`/`Usage` or pass it in from\nthe call site (the command is already built before help is written in normal\nflow). Alternatively, expose a method to check `is_built()` and skip the\nclone+build when the command is already in a built state.","locations":[{"ref":"clap_builder/src/output/help_template.rs:462-466","code":" 462 | if subcmds && flatten {\n 463 | let mut cmd = self.cmd.clone();\n 464 | cmd.build();\n 465 | self.write_flat_subcommands(&cmd, &mut first);\n 466 | }","lines":5},{"ref":"clap_builder/src/output/usage.rs:114-115","code":" 114 | let mut cmd = self.cmd.clone();\n 115 | cmd.build();","lines":2}]},{"id":"PERF_6","type":"performance","type_label":"Performance","severity":"low","easy":false,"description":"In `did_you_mean` in suggestions.rs, `candidates.insert(pos, ...)` is called with a position computed by binary search. Vec::insert is O(n) due to shifting. For a large number of possible values, this results in O(n^2) total insertion cost. For typical CLIs this is negligible, but for completion systems or commands with hundreds of subcommands it could be visible.","fix":"Collect all candidates into a Vec and sort at the end instead of maintaining sorted order during insertion.","locations":[{"ref":"clap_builder/src/parser/features/suggestions.rs:23-34","code":" 23 | let pos = candidates\n 24 | .binary_search_by(|probe| {\n 25 | if probe.0 > confidence {\n 26 | Ordering::Greater\n 27 | } else {\n 28 | Ordering::Less\n 29 | }\n 30 | })\n 31 | .unwrap_or_else(|e| e);\n 32 | candidates.insert(pos, new_elem);\n 33 | }\n 34 | }","lines":12}]},{"id":"PERF_7","type":"performance","type_label":"Performance","severity":"low","easy":false,"description":"In `FlatSet::insert`, every insertion does a linear scan for duplicates (O(n)). This is used in `build_conflict_err` (via `collect::<FlatSet<_>>()`) to deduplicate potentially large conflict lists. For conflict lists with many entries, this is O(n^2).","fix":"Sort and deduplicate with `sort_by`+`dedup` on a plain `Vec` for larger sets, or accept the O(n^2) cost given that conflict lists are typically small.","locations":[{"ref":"clap_builder/src/util/flat_set.rs:18-25","code":" 18 | pub(crate) fn insert(&mut self, value: T) -> bool {\n 19 | for existing in &self.inner {\n 20 | if *existing == value {\n 21 | return false;\n 22 | }\n 23 | }\n 24 | self.inner.push(value);\n 25 | true","lines":8},{"ref":"clap_builder/src/parser/validator.rs:135-145","code":" 135 | let conflict_ids = conflict_ids\n 136 | .iter()\n 137 | .flat_map(|c_id| {\n 138 | if self.cmd.find_group(c_id).is_some() {\n 139 | self.cmd.unroll_args_in_group(c_id)\n 140 | } else {\n 141 | vec![c_id.clone()]\n 142 | }\n 143 | })\n 144 | .collect::<FlatSet<_>>()\n 145 | .into_vec();","lines":11}]},{"id":"DEAD_1","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"`num_vals_last_group` in `MatchedArg` is marked `#[allow(dead_code)]` and is never called anywhere in the codebase. The comment says 'will be used later', but this has been present in the code for a long time.","fix":"Remove the method or remove the `#[allow(dead_code)]` attribute and address the resulting compiler warning by either using the method or deleting it.","locations":[{"ref":"clap_builder/src/parser/matches/matched_arg.rs:129-133","code":" 129 | // Will be used later\n 130 | #[allow(dead_code)]\n 131 | pub(crate) fn num_vals_last_group(&self) -> usize {\n 132 | self.vals.last().map(|x| x.len()).unwrap_or(0)\n 133 | }","lines":5}]},{"id":"DEAD_2","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"The entire `flat_map.rs` and `flat_set.rs` files start with `#![allow(dead_code)]`, suppressing warnings for all unused items in those files. Several iterator methods in both files (like `iter_mut`, `sort_by_key`, `retain`) may not be used outside those files.","fix":"Remove the blanket `#![allow(dead_code)]` and address individual items - either add `#[allow(dead_code)]` per item with justification, or remove unused items.","locations":[{"ref":"clap_builder/src/util/flat_map.rs:1","code":" 1 | #![allow(dead_code)]","lines":1},{"ref":"clap_builder/src/util/flat_set.rs:1","code":" 1 | #![allow(dead_code)]","lines":1}]},{"id":"DEAD_3","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"In the same `complete_option` function, the `arg.is_empty()` branch (lines 250-263) contains a dead sub-expression. The `if arg.is_empty()` check at line 254 (inside the outer `if arg.is_empty()` block) is always true - its `else` arm (line 256-258) can never execute. This is duplicate/dead code.","fix":"Replace the inner conditional with just `\"-\".into()` since the condition is always true in this context.","locations":[{"ref":"clap_complete/src/engine/complete.rs:250-263","code":" 250 | if arg.is_empty() {\n 251 | completions.extend(longs_and_visible_aliases(cmd));\n 252 | completions.extend(hidden_longs_aliases(cmd));\n 253 | \n 254 | let dash_or_arg = if arg.is_empty() {\n 255 | \"-\".into()\n 256 | } else {\n 257 | arg.to_value_os().to_string_lossy()\n 258 | };\n 259 | completions.extend(\n 260 | shorts_and_visible_aliases(cmd)\n 261 | .into_iter()\n 262 | .map(|comp| comp.add_prefix(dash_or_arg.to_string())),\n 263 | );","lines":14}]},{"id":"DEAD_4","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"In `engine/complete.rs` inside `complete_option`, the `else if arg.is_stdio()` branch (lines 264-278) contains a dead sub-expression. At line 266, `if arg.is_empty()` is checked inside a branch that can only be reached when `arg.is_empty()` is false (the `is_empty()` check at line 250 acts as the first branch). So the condition at line 266 is always false and the `else` arm at line 268-270 is the only path ever taken; the `if` at line 266 and its then-arm are unreachable dead code.","fix":"Remove the inner `if arg.is_empty()` guard and replace `dash_or_arg` with the direct `arg.to_value_os().to_string_lossy()` value, simplifying the branch.","locations":[{"ref":"clap_complete/src/engine/complete.rs:264-278","code":" 264 | } else if arg.is_stdio() {\n 265 | // HACK: Assuming knowledge of is_stdio\n 266 | let dash_or_arg = if arg.is_empty() {\n 267 | \"-\".into()\n 268 | } else {\n 269 | arg.to_value_os().to_string_lossy()\n 270 | };\n 271 | completions.extend(\n 272 | shorts_and_visible_aliases(cmd)\n 273 | .into_iter()\n 274 | .map(|comp| comp.add_prefix(dash_or_arg.to_string())),\n 275 | );\n 276 | \n 277 | completions.extend(longs_and_visible_aliases(cmd));\n 278 | completions.extend(hidden_longs_aliases(cmd));","lines":15}]},{"id":"DEAD_5","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"`gen_to_possible_value` in `value_enum.rs` generates a `match self` that\ncovers all enum variants explicitly but still has a final `_ => None` wildcard\narm (line 129). Since `ValueEnum` can only be derived on enums with only unit\nvariants, and all non-skipped variants are listed explicitly, the wildcard is\nalways unreachable. Rust will emit an `unreachable_patterns` warning for the\ngenerated code (suppressed by the blanket `#[allow(dead_code)]` in the\nsurrounding `impl` block, masking the issue).","fix":"Remove the `_ => None` arm. All variants are explicitly covered; the wildcard\nis structurally unreachable and only adds noise.","locations":[{"ref":"clap_derive/src/derives/value_enum.rs:119-133","code":" 119 | fn gen_to_possible_value(item: &Item, lits: &[(TokenStream, Ident)]) -> TokenStream {\n 120 | let (lit, variant): (Vec<TokenStream>, Vec<Ident>) = lits.iter().cloned().unzip();\n 121 | \n 122 | let deprecations = item.deprecations();\n 123 | \n 124 | quote! {\n 125 | fn to_possible_value<'a>(&self) -> ::std::option::Option<clap::builder::PossibleValue> {\n 126 | #deprecations\n 127 | match self {\n 128 | #(Self::#variant => Some(#lit),)*\n 129 | _ => None\n 130 | }\n 131 | }\n 132 | }\n 133 | }","lines":15}]},{"id":"DEAD_6","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"Three match arms in `Item::infer_kind` contain an inner `if attr.value.is_some()`\nblock that can never be reached because the outer guard already requires\n`attr.value.is_none()` for those arms:\n- `Some(MagicAttrName::Subcommand) if attr.value.is_none()` (line 369) contains\n `if attr.value.is_some()` (line 370-373) - always false.\n- `Some(MagicAttrName::ExternalSubcommand) if attr.value.is_none()` (line 382)\n contains `if attr.value.is_some()` (line 383-386) - always false.\n- `Some(MagicAttrName::Flatten) if attr.value.is_none()` (line 390) contains\n `if attr.value.is_some()` (line 391-394) - always false.\n\nThe `FromGlobal` arm at line 356-360 lacks the `if attr.value.is_none()` guard\nbut DOES check `if attr.value.is_some()` - that check is reachable and correct\nthere. The pattern appears to be a copy-paste from `FromGlobal` into the other\nthree arms without removing the now-unreachable check.","fix":"Remove the inner `if attr.value.is_some()` blocks from the `Subcommand`,\n`ExternalSubcommand`, and `Flatten` arms. They are completely unreachable\nand create misleading code.","locations":[{"ref":"clap_derive/src/item.rs:369-380","code":" 369 | Some(MagicAttrName::Subcommand) if attr.value.is_none() => {\n 370 | if attr.value.is_some() {\n 371 | let expr = attr.value_or_abort()?;\n 372 | abort!(expr, \"attribute `{}` does not accept a value\", attr.name);\n 373 | }\n 374 | let ty = self\n 375 | .kind()\n 376 | .ty()\n 377 | .cloned()\n 378 | .unwrap_or_else(|| Sp::new(Ty::Other, self.kind.span()));\n 379 | let kind = Sp::new(Kind::Subcommand(ty), attr.name.span());\n 380 | Some(kind)","lines":12},{"ref":"clap_derive/src/item.rs:382-388","code":" 382 | Some(MagicAttrName::ExternalSubcommand) if attr.value.is_none() => {\n 383 | if attr.value.is_some() {\n 384 | let expr = attr.value_or_abort()?;\n 385 | abort!(expr, \"attribute `{}` does not accept a value\", attr.name);\n 386 | }\n 387 | let kind = Sp::new(Kind::ExternalSubcommand, attr.name.span());\n 388 | Some(kind)","lines":7},{"ref":"clap_derive/src/item.rs:390-401","code":" 390 | Some(MagicAttrName::Flatten) if attr.value.is_none() => {\n 391 | if attr.value.is_some() {\n 392 | let expr = attr.value_or_abort()?;\n 393 | abort!(expr, \"attribute `{}` does not accept a value\", attr.name);\n 394 | }\n 395 | let ty = self\n 396 | .kind()\n 397 | .ty()\n 398 | .cloned()\n 399 | .unwrap_or_else(|| Sp::new(Ty::Other, self.kind.span()));\n 400 | let kind = Sp::new(Kind::Flatten(ty), attr.name.span());\n 401 | Some(kind)","lines":12}]},{"id":"DEAD_7","type":"dead_code","type_label":"Dead Code","severity":"negligible","easy":true,"description":"AppSettings::ColorAuto is marked #[allow(dead_code)], indicating it is defined but never used. It occupies a bit in the bitfield and contributes to the risk of overflowing the 32-variant limit.","fix":"Remove ColorAuto if it is truly unused, or add the missing code paths that set/check it.","locations":[{"ref":"clap_builder/src/builder/app_settings.rs:76-77","code":" 76 | #[allow(dead_code)]\n 77 | ColorAuto,","lines":2}]},{"id":"CMT_1","type":"invalid_comment","type_label":"Invalid Comment","severity":"low","easy":true,"description":"Three getter methods have copy-pasted doc comments that say \"Get the name of the binary.\" but the\nmethods return completely different values: `get_display_name` returns the display name,\n`get_bin_name` returns the binary name, and `get_bin_name_fallback` returns either the binary name\nor the command name. All three share the same incorrect single-line summary copied from the method\nabove them.","fix":"Write correct one-line summaries:\n- `get_display_name`: \"Get the display name of the command, if set.\"\n- `get_bin_name`: \"Get the binary name of the command, if set.\"\n- `get_bin_name_fallback`: \"Get the binary name of the command, falling back to the command name.\"","locations":[{"ref":"clap_builder/src/builder/command.rs:3731-3747","code":" 3731 | /// Get the name of the binary.\n 3732 | #[inline]\n 3733 | pub fn get_display_name(&self) -> Option<&str> {\n 3734 | self.display_name.as_deref()\n 3735 | }\n 3736 | \n 3737 | /// Get the name of the binary.\n 3738 | #[inline]\n 3739 | pub fn get_bin_name(&self) -> Option<&str> {\n 3740 | self.bin_name.as_deref()\n 3741 | }\n 3742 | \n 3743 | /// Get the name of the binary.\n 3744 | #[inline]\n 3745 | pub(crate) fn get_bin_name_fallback(&self) -> &str {\n 3746 | self.bin_name.as_deref().unwrap_or_else(|| self.get_name())\n 3747 | }","lines":17}]},{"id":"CMT_2","type":"invalid_comment","type_label":"Invalid Comment","severity":"low","easy":true,"description":"Four getter methods (`get_before_help`, `get_before_long_help`, `get_after_help`,\n`get_after_long_help`) all have the same incorrect doc summary \"Returns the help heading for\nlisting subcommands.\" That text describes a completely unrelated method. These getters return the\nbefore/after help text strings, not a heading.","fix":"Replace with accurate summaries:\n- `get_before_help`: \"Get the text displayed before the help message.\"\n- `get_before_long_help`: \"Get the text displayed before the long help message.\"\n- `get_after_help`: \"Get the text displayed after the help message.\"\n- `get_after_long_help`: \"Get the text displayed after the long help message.\"","locations":[{"ref":"clap_builder/src/builder/command.rs:3952-3974","code":" 3952 | /// Returns the help heading for listing subcommands.\n 3953 | #[inline]\n 3954 | pub fn get_before_help(&self) -> Option<&StyledStr> {\n 3955 | self.before_help.as_ref()\n 3956 | }\n 3957 | \n 3958 | /// Returns the help heading for listing subcommands.\n 3959 | #[inline]\n 3960 | pub fn get_before_long_help(&self) -> Option<&StyledStr> {\n 3961 | self.before_long_help.as_ref()\n 3962 | }\n 3963 | \n 3964 | /// Returns the help heading for listing subcommands.\n 3965 | #[inline]\n 3966 | pub fn get_after_help(&self) -> Option<&StyledStr> {\n 3967 | self.after_help.as_ref()\n 3968 | }\n 3969 | \n 3970 | /// Returns the help heading for listing subcommands.\n 3971 | #[inline]\n 3972 | pub fn get_after_long_help(&self) -> Option<&StyledStr> {\n 3973 | self.after_long_help.as_ref()\n 3974 | }","lines":23}]},{"id":"CMT_3","type":"invalid_comment","type_label":"Invalid Comment","severity":"low","easy":true,"description":"Two FIXME comments sit on public API methods `get_arg_conflicts_with` and\n`get_global_arg_conflicts_with`: `// FIXME: This could probably have been an iterator`. These\nstale notes are visible in generated rustdoc via `//!` if expanded, and signal unfinished design\nwork on a stable public API surface.","fix":"Either convert the return type to `impl Iterator<Item = &Arg>` (resolving the FIXME) or remove\nthe comment if the Vec return is intentional.","locations":[{"ref":"clap_builder/src/builder/command.rs:4031-4032","code":" 4031 | pub fn get_arg_conflicts_with(&self, arg: &Arg) -> Vec<&Arg> // FIXME: This could probably have been an iterator\n 4032 | {","lines":2},{"ref":"clap_builder/src/builder/command.rs:4065-4066","code":" 4065 | fn get_global_arg_conflicts_with(&self, arg: &Arg) -> Vec<&Arg> // FIXME: This could probably have been an iterator\n 4066 | {","lines":2}]},{"id":"CMT_4","type":"invalid_comment","type_label":"Invalid Comment","severity":"low","easy":true,"description":"`RawArgs::new` has a doc comment that starts with four slashes (`////`)\ninstead of three (`///`) on the first line (lib.rs line 134):\n\n //// Create an argument list to parse\n\nSimilarly `RawArgs::from_args` at line 133 has the same four-slash comment\nopening. Four slashes create an inner doc comment (`//!`) rather than the\nintended outer item doc comment (`///`), except that `////` is actually just\nparsed as a regular line comment (starting with `//`) containing `// Create...`.\nThe doc comment is therefore silently dropped from the public API documentation.","fix":"Change `////` to `///` on both comment lines so they are recognized as rustdoc\nouter doc comments and appear in the generated API docs.","locations":[{"ref":"clap_lex/src/lib.rs:133-134","code":" 133 | impl RawArgs {\n 134 | //// Create an argument list to parse","lines":2},{"ref":"clap_lex/src/lib.rs:157-158","code":" 157 | //// Create an argument list to parse\n 158 | ///","lines":2}]},{"id":"CMT_5","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","easy":true,"description":"Three typos in public doc comments in command.rs:\n- Line 2548: \"nofice\" should be \"notice\"\n- Line 3526: \"subcommmand\" (triple 'm') should be \"subcommand\"\n- Line 4096: \"Sucommand_1\" should be \"Subcommand_1\"","fix":"Fix the three spelling errors in the doc strings.","locations":[{"ref":"clap_builder/src/builder/command.rs:2548","code":" 2548 | /// Now nofice if we don't specify `foo` or `baz` but use the `--` operator.","lines":1},{"ref":"clap_builder/src/builder/command.rs:3526","code":" 3526 | /// with a subcommmand for each applet that can be run directly,","lines":1},{"ref":"clap_builder/src/builder/command.rs:4096","code":" 4096 | /// **NOTE:** In this case only `Sucommand_1` will be included","lines":1}]},{"id":"CMT_6","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","easy":true,"description":"Two stale developer notes `// maybe Some(m)` remain in production code inside\n`_build_subcommand` and `_build_bin_names_internal`. These were exploratory comments during\ndevelopment and convey no useful information to a reader.","fix":"Remove both comments.","locations":[{"ref":"clap_builder/src/builder/command.rs:4501","code":" 4501 | let reqs = Usage::new(self).get_required_usage_from(&[], None, true); // maybe Some(m)","lines":1},{"ref":"clap_builder/src/builder/command.rs:4588","code":" 4588 | let reqs = Usage::new(self).get_required_usage_from(&[], None, true); // maybe Some(m)","lines":1}]},{"id":"CMT_7","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","easy":true,"description":"In range.rs, the doc comment for ValueRange::EMPTY says \"Nor argument values, or a flag\" which is a typo. It should read \"No argument values, or a flag\".","fix":"Change \"Nor argument values\" to \"No argument values\".","locations":[{"ref":"clap_builder/src/builder/range.rs:9","code":" 9 | /// Nor argument values, or a flag","lines":1}]},{"id":"CMT_8","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","easy":true,"description":"In `arg_matches.rs`, the doc-comment on `get_raw` at line 310 has a typo: '[`OsSt`]' should be '[`OsStr`]'.","fix":"Change '[`OsSt`]' to '[`OsStr`]'.","locations":[{"ref":"clap_builder/src/parser/matches/arg_matches.rs:310","code":" 310 | /// [`OsSt`]: std::ffi::OsStr","lines":1}]},{"id":"CMT_9","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","easy":true,"description":"In `parser.rs` line 1000, the comment says 'formentioned' which is a misspelling of 'aforementioned'.","fix":"Change 'formentioned' to 'aforementioned'.","locations":[{"ref":"clap_builder/src/parser/parser.rs:1000","code":" 1000 | // If it is a new flag subcommand, then the formentioned index should be the current one","lines":1}]},{"id":"CMT_10","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","easy":true,"description":"In `aot/generator/mod.rs` at line 72, the `try_generate` method has a leading blank line before its doc comment triple-slash: `///` starts at line 72 but the actual text starts at line 73 with an empty `///` and then `/// Fallible version...`. The extra leading blank `///` at line 72 produces an empty paragraph in the rendered documentation, which looks like a formatting artifact.","fix":"Remove the leading blank `///` line at line 72.","locations":[{"ref":"clap_complete/src/aot/generator/mod.rs:72-73","code":" 72 | ///\n 73 | /// Fallible version to generate output out of [`clap::Command`].","lines":2}]},{"id":"DUP_1","type":"duplicate_code","type_label":"Duplicate Code","severity":"medium","easy":false,"description":"The logic for computing `sc_names`, `bin_name`, `display_name`, and `usage_name` for each\nsubcommand is implemented twice: once inside `_build_subcommand()` (the recursive build path) and\nagain, almost identically, inside `_build_bin_names_internal()`. The two blocks span roughly 50\nlines each and contain the same conditional chains, string formatting, and assignments. Any bug fix\nor behaviour change must be applied in both places, and the two copies have already diverged\n(see the `usage_name` fallback finding above).","fix":"Extract the shared name-derivation logic into a private helper method such as\n`fn derive_subcommand_names(parent_bin: Option<&str>, sc_name: &str, mid_string: &str) -> (String, String, String)`\nand call it from both `_build_subcommand` and `_build_bin_names_internal`.","locations":[{"ref":"clap_builder/src/builder/command.rs:4494-4577","code":" 4494 | pub(crate) fn _build_subcommand(&mut self, name: &str) -> Option<&mut Self> {\n 4495 | use std::fmt::Write;\n 4496 | \n 4497 | let mut mid_string = String::from(\" \");\n 4498 | #[cfg(feature = \"usage\")]\n 4499 | if !self.is_subcommand_negates_reqs_set() && !self.is_args_conflicts_with_subcommands_set()\n 4500 | {\n 4501 | let reqs = Usage::new(self).get_required_usage_from(&[], None, true); // maybe Some(m)\n 4502 | \n 4503 | for s in &reqs {\n 4504 | mid_string.push_str(&s.to_string());\n 4505 | mid_string.push(' ');\n 4506 | }\n 4507 | }\n 4508 | let is_multicall_set = self.is_multicall_set();\n 4509 | \n 4510 | let sc = some!(self.subcommands.iter_mut().find(|s| s.name == name));\n 4511 | \n 4512 | // Display subcommand name, short and long in usage\n 4513 | let mut sc_names = String::new();\n 4514 | sc_names.push_str(sc.name.as_str());\n 4515 | let mut flag_subcmd = false;\n 4516 | if let Some(l) = sc.get_long_flag() {\n 4517 | write!(sc_names, \"|--{l}\").unwrap();\n 4518 | flag_subcmd = true;\n 4519 | }\n 4520 | if let Some(s) = sc.get_short_flag() {\n 4521 | write!(sc_names, \"|-{s}\").unwrap();\n 4522 | flag_subcmd = true;\n 4523 | }\n 4524 | \n 4525 | if flag_subcmd {\n 4526 | sc_names = format!(\"{{{sc_names}}}\");\n 4527 | }\n 4528 | \n 4529 | let usage_name = self\n 4530 | .bin_name\n 4531 | .as_ref()\n 4532 | .map(|bin_name| format!(\"{bin_name}{mid_string}{sc_names}\"))\n 4533 | .unwrap_or(sc_names);\n 4534 | sc.usage_name = Some(usage_name);\n 4535 | \n 4536 | // bin_name should be parent's bin_name + [<reqs>] + the sc's name separated by\n 4537 | // a space\n 4538 | let bin_name = format!(\n 4539 | \"{}{}{}\",\n 4540 | self.bin_name.as_deref().unwrap_or_default(),\n 4541 | if self.bin_name.is_some() { \" \" } else { \"\" },\n 4542 | &*sc.name\n 4543 | );\n 4544 | debug!(\n 4545 | \"Command::_build_subcommand Setting bin_name of {} to {:?}\",\n 4546 | sc.name, bin_name\n 4547 | );\n 4548 | sc.bin_name = Some(bin_name);\n 4549 | \n 4550 | if sc.display_name.is_none() {\n 4551 | let self_display_name = if is_multicall_set {\n 4552 | self.display_name.as_deref().unwrap_or(\"\")\n 4553 | } else {\n 4554 | self.display_name.as_deref().unwrap_or(&self.name)\n 4555 | };\n 4556 | let display_name = format!(\n 4557 | \"{}{}{}\",\n 4558 | self_display_name,\n 4559 | if !self_display_name.is_empty() {\n 4560 | \"-\"\n 4561 | } else {\n 4562 | \"\"\n 4563 | },\n 4564 | &*sc.name\n 4565 | );\n 4566 | debug!(\n 4567 | \"Command::_build_subcommand Setting display_name of {} to {:?}\",\n 4568 | sc.name, display_name\n 4569 | );\n 4570 | sc.display_name = Some(display_name);\n 4571 | }\n 4572 | \n 4573 | // Ensure all args are built and ready to parse\n 4574 | sc._build_self(false);\n 4575 | \n 4576 | Some(sc)\n 4577 | }","lines":84},{"ref":"clap_builder/src/builder/command.rs:4579-4692","code":" 4579 | fn _build_bin_names_internal(&mut self) {\n 4580 | debug!(\"Command::_build_bin_names\");\n 4581 | \n 4582 | if !self.is_set(AppSettings::BinNameBuilt) {\n 4583 | let mut mid_string = String::from(\" \");\n 4584 | #[cfg(feature = \"usage\")]\n 4585 | if !self.is_subcommand_negates_reqs_set()\n 4586 | && !self.is_args_conflicts_with_subcommands_set()\n 4587 | {\n 4588 | let reqs = Usage::new(self).get_required_usage_from(&[], None, true); // maybe Some(m)\n 4589 | \n 4590 | for s in &reqs {\n 4591 | mid_string.push_str(&s.to_string());\n 4592 | mid_string.push(' ');\n 4593 | }\n 4594 | }\n 4595 | let is_multicall_set = self.is_multicall_set();\n 4596 | \n 4597 | let self_bin_name = if is_multicall_set {\n 4598 | self.bin_name.as_deref().unwrap_or(\"\")\n 4599 | } else {\n 4600 | self.bin_name.as_deref().unwrap_or(&self.name)\n 4601 | }\n 4602 | .to_owned();\n 4603 | \n 4604 | for sc in &mut self.subcommands {\n 4605 | debug!(\"Command::_build_bin_names:iter: bin_name set...\");\n 4606 | \n 4607 | if sc.usage_name.is_none() {\n 4608 | use std::fmt::Write;\n 4609 | // Display subcommand name, short and long in usage\n 4610 | let mut sc_names = String::new();\n 4611 | sc_names.push_str(sc.name.as_str());\n 4612 | let mut flag_subcmd = false;\n 4613 | if let Some(l) = sc.get_long_flag() {\n 4614 | write!(sc_names, \"|--{l}\").unwrap();\n 4615 | flag_subcmd = true;\n 4616 | }\n 4617 | if let Some(s) = sc.get_short_flag() {\n 4618 | write!(sc_names, \"|-{s}\").unwrap();\n 4619 | flag_subcmd = true;\n 4620 | }\n 4621 | \n 4622 | if flag_subcmd {\n 4623 | sc_names = format!(\"{{{sc_names}}}\");\n 4624 | }\n 4625 | \n 4626 | let usage_name = format!(\"{self_bin_name}{mid_string}{sc_names}\");\n 4627 | debug!(\n 4628 | \"Command::_build_bin_names:iter: Setting usage_name of {} to {:?}\",\n 4629 | sc.name, usage_name\n 4630 | );\n 4631 | sc.usage_name = Some(usage_name);\n 4632 | } else {\n 4633 | debug!(\n 4634 | \"Command::_build_bin_names::iter: Using existing usage_name of {} ({:?})\",\n 4635 | sc.name, sc.usage_name\n 4636 | );\n 4637 | }\n 4638 | \n 4639 | if sc.bin_name.is_none() {\n 4640 | let bin_name = format!(\n 4641 | \"{}{}{}\",\n 4642 | self_bin_name,\n 4643 | if !self_bin_name.is_empty() { \" \" } else { \"\" },\n 4644 | &*sc.name\n 4645 | );\n 4646 | debug!(\n 4647 | \"Command::_build_bin_names:iter: Setting bin_name of {} to {:?}\",\n 4648 | sc.name, bin_name\n 4649 | );\n 4650 | sc.bin_name = Some(bin_name);\n 4651 | } else {\n 4652 | debug!(\n 4653 | \"Command::_build_bin_names::iter: Using existing bin_name of {} ({:?})\",\n 4654 | sc.name, sc.bin_name\n 4655 | );\n 4656 | }\n 4657 | \n 4658 | if sc.display_name.is_none() {\n 4659 | let self_display_name = if is_multicall_set {\n 4660 | self.display_name.as_deref().unwrap_or(\"\")\n 4661 | } else {\n 4662 | self.display_name.as_deref().unwrap_or(&self.name)\n 4663 | };\n 4664 | let display_name = format!(\n 4665 | \"{}{}{}\",\n 4666 | self_display_name,\n 4667 | if !self_display_name.is_empty() {\n 4668 | \"-\"\n 4669 | } else {\n 4670 | \"\"\n 4671 | },\n 4672 | &*sc.name\n 4673 | );\n 4674 | debug!(\n 4675 | \"Command::_build_bin_names:iter: Setting display_name of {} to {:?}\",\n 4676 | sc.name, display_name\n 4677 | );\n 4678 | sc.display_name = Some(display_name);\n 4679 | } else {\n 4680 | debug!(\n 4681 | \"Command::_build_bin_names::iter: Using existing display_name of {} ({:?})\",\n 4682 | sc.name, sc.display_name\n 4683 | );\n 4684 | }\n 4685 | \n 4686 | sc._build_bin_names_internal();\n 4687 | }\n 4688 | self.set(AppSettings::BinNameBuilt);\n 4689 | } else {\n 4690 | debug!(\"Command::_build_bin_names: already built\");\n 4691 | }\n 4692 | }","lines":114}]},{"id":"DUP_2","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","easy":false,"description":"RangedI64ValueParser and RangedU64ValueParser are structurally identical except for the primitive type (i64 vs u64). The range() method (48 lines each), format_bounds() (21 lines each), and parse_ref() (50 lines each) contain near-identical logic duplicated across both structs. Any bug fix or feature addition must be applied in two places.","fix":"Extract a shared generic helper (e.g. a macro or a private generic fn) for range narrowing and bounds formatting, or unify the two parsers via a sealed trait over the integer type.","locations":[{"ref":"clap_builder/src/builder/value_parser.rs:1320-1467","code":" 1320 | impl<T: TryFrom<i64> + Clone + Send + Sync> RangedI64ValueParser<T> {\n 1321 | /// Select full range of `i64`\n 1322 | pub fn new() -> Self {\n 1323 | Self::from(..)\n 1324 | }\n 1325 | \n 1326 | /// Narrow the supported range\n 1327 | pub fn range<B: RangeBounds<i64>>(mut self, range: B) -> Self {\n 1328 | // Consideration: when the user does `value_parser!(u8).range()`\n 1329 | // - Avoid programming mistakes by accidentally expanding the range\n 1330 | // - Make it convenient to limit the range like with `..10`\n 1331 | let start = match range.start_bound() {\n 1332 | l @ std::ops::Bound::Included(i) => {\n 1333 | debug_assert!(\n 1334 | self.bounds.contains(i),\n 1335 | \"{} must be in {:?}\",\n 1336 | i,\n 1337 | self.bounds\n 1338 | );\n 1339 | l.cloned()\n 1340 | }\n 1341 | l @ std::ops::Bound::Excluded(i) => {\n 1342 | debug_assert!(\n 1343 | self.bounds.contains(&i.saturating_add(1)),\n 1344 | \"{} must be in {:?}\",\n 1345 | i,\n 1346 | self.bounds\n 1347 | );\n 1348 | l.cloned()\n 1349 | }\n 1350 | std::ops::Bound::Unbounded => self.bounds.start_bound().cloned(),\n 1351 | };\n 1352 | let end = match range.end_bound() {\n 1353 | l @ std::ops::Bound::Included(i) => {\n 1354 | debug_assert!(\n 1355 | self.bounds.contains(i),\n 1356 | \"{} must be in {:?}\",\n 1357 | i,\n 1358 | self.bounds\n 1359 | );\n 1360 | l.cloned()\n 1361 | }\n 1362 | l @ std::ops::Bound::Excluded(i) => {\n 1363 | debug_assert!(\n 1364 | self.bounds.contains(&i.saturating_sub(1)),\n 1365 | \"{} must be in {:?}\",\n 1366 | i,\n 1367 | self.bounds\n 1368 | );\n 1369 | l.cloned()\n 1370 | }\n 1371 | std::ops::Bound::Unbounded => self.bounds.end_bound().cloned(),\n 1372 | };\n 1373 | self.bounds = (start, end);\n 1374 | self\n 1375 | }\n 1376 | \n 1377 | fn format_bounds(&self) -> String {\n 1378 | let mut result = match self.bounds.0 {\n 1379 | std::ops::Bound::Included(i) => i.to_string(),\n 1380 | std::ops::Bound::Excluded(i) => i.saturating_add(1).to_string(),\n 1381 | std::ops::Bound::Unbounded => i64::MIN.to_string(),\n 1382 | };\n 1383 | result.push_str(\"..\");\n 1384 | match self.bounds.1 {\n 1385 | std::ops::Bound::Included(i) => {\n 1386 | result.push('=');\n 1387 | result.push_str(&i.to_string());\n 1388 | }\n 1389 | std::ops::Bound::Excluded(i) => {\n 1390 | result.push_str(&i.to_string());\n 1391 | }\n 1392 | std::ops::Bound::Unbounded => {\n 1393 | result.push_str(&i64::MAX.to_string());\n 1394 | }\n 1395 | }\n 1396 | result\n 1397 | }\n 1398 | }\n 1399 | \n 1400 | impl<T: TryFrom<i64> + Clone + Send + Sync + 'static> TypedValueParser for RangedI64ValueParser<T>\n 1401 | where\n 1402 | <T as TryFrom<i64>>::Error: Send + Sync + 'static + std::error::Error + ToString,\n 1403 | {\n 1404 | type Value = T;\n 1405 | \n 1406 | fn parse_ref(\n 1407 | &self,\n 1408 | cmd: &crate::Command,\n 1409 | arg: Option<&crate::Arg>,\n 1410 | raw_value: &std::ffi::OsStr,\n 1411 | ) -> Result<Self::Value, crate::Error> {\n 1412 | let value = ok!(raw_value.to_str().ok_or_else(|| {\n 1413 | crate::Error::invalid_utf8(\n 1414 | cmd,\n 1415 | crate::output::Usage::new(cmd).create_usage_with_title(&[]),\n 1416 | )\n 1417 | }));\n 1418 | let value = ok!(value.parse::<i64>().map_err(|err| {\n 1419 | let arg = arg\n 1420 | .map(|a| a.to_string())\n 1421 | .unwrap_or_else(|| \"...\".to_owned());\n 1422 | crate::Error::value_validation(\n 1423 | arg,\n 1424 | raw_value.to_string_lossy().into_owned(),\n 1425 | err.into(),\n 1426 | )\n 1427 | .with_cmd(cmd)\n 1428 | }));\n 1429 | if !self.bounds.contains(&value) {\n 1430 | let arg = arg\n 1431 | .map(|a| a.to_string())\n 1432 | .unwrap_or_else(|| \"...\".to_owned());\n 1433 | return Err(crate::Error::value_validation(\n 1434 | arg,\n 1435 | raw_value.to_string_lossy().into_owned(),\n 1436 | format!(\"{} is not in {}\", value, self.format_bounds()).into(),\n 1437 | )\n 1438 | .with_cmd(cmd));\n 1439 | }\n 1440 | \n 1441 | let value: Result<Self::Value, _> = value.try_into();\n 1442 | let value = ok!(value.map_err(|err| {\n 1443 | let arg = arg\n 1444 | .map(|a| a.to_string())\n 1445 | .unwrap_or_else(|| \"...\".to_owned());\n 1446 | crate::Error::value_validation(\n 1447 | arg,\n 1448 | raw_value.to_string_lossy().into_owned(),\n 1449 | err.into(),\n 1450 | )\n 1451 | .with_cmd(cmd)\n 1452 | }));\n 1453 | \n 1454 | Ok(value)\n 1455 | }\n 1456 | }\n 1457 | \n 1458 | impl<T: TryFrom<i64> + Clone + Send + Sync, B: RangeBounds<i64>> From<B>\n 1459 | for RangedI64ValueParser<T>\n 1460 | {\n 1461 | fn from(range: B) -> Self {\n 1462 | Self {\n 1463 | bounds: (range.start_bound().cloned(), range.end_bound().cloned()),\n 1464 | target: Default::default(),\n 1465 | }\n 1466 | }\n 1467 | }","lines":148},{"ref":"clap_builder/src/builder/value_parser.rs:1519-1655","code":" 1519 | impl<T: TryFrom<u64>> RangedU64ValueParser<T> {\n 1520 | /// Select full range of `u64`\n 1521 | pub fn new() -> Self {\n 1522 | Self::from(..)\n 1523 | }\n 1524 | \n 1525 | /// Narrow the supported range\n 1526 | pub fn range<B: RangeBounds<u64>>(mut self, range: B) -> Self {\n 1527 | // Consideration: when the user does `value_parser!(u8).range()`\n 1528 | // - Avoid programming mistakes by accidentally expanding the range\n 1529 | // - Make it convenient to limit the range like with `..10`\n 1530 | let start = match range.start_bound() {\n 1531 | l @ std::ops::Bound::Included(i) => {\n 1532 | debug_assert!(\n 1533 | self.bounds.contains(i),\n 1534 | \"{} must be in {:?}\",\n 1535 | i,\n 1536 | self.bounds\n 1537 | );\n 1538 | l.cloned()\n 1539 | }\n 1540 | l @ std::ops::Bound::Excluded(i) => {\n 1541 | debug_assert!(\n 1542 | self.bounds.contains(&i.saturating_add(1)),\n 1543 | \"{} must be in {:?}\",\n 1544 | i,\n 1545 | self.bounds\n 1546 | );\n 1547 | l.cloned()\n 1548 | }\n 1549 | std::ops::Bound::Unbounded => self.bounds.start_bound().cloned(),\n 1550 | };\n 1551 | let end = match range.end_bound() {\n 1552 | l @ std::ops::Bound::Included(i) => {\n 1553 | debug_assert!(\n 1554 | self.bounds.contains(i),\n 1555 | \"{} must be in {:?}\",\n 1556 | i,\n 1557 | self.bounds\n 1558 | );\n 1559 | l.cloned()\n 1560 | }\n 1561 | l @ std::ops::Bound::Excluded(i) => {\n 1562 | debug_assert!(\n 1563 | self.bounds.contains(&i.saturating_sub(1)),\n 1564 | \"{} must be in {:?}\",\n 1565 | i,\n 1566 | self.bounds\n 1567 | );\n 1568 | l.cloned()\n 1569 | }\n 1570 | std::ops::Bound::Unbounded => self.bounds.end_bound().cloned(),\n 1571 | };\n 1572 | self.bounds = (start, end);\n 1573 | self\n 1574 | }\n 1575 | \n 1576 | fn format_bounds(&self) -> String {\n 1577 | let mut result = match self.bounds.0 {\n 1578 | std::ops::Bound::Included(i) => i.to_string(),\n 1579 | std::ops::Bound::Excluded(i) => i.saturating_add(1).to_string(),\n 1580 | std::ops::Bound::Unbounded => u64::MIN.to_string(),\n 1581 | };\n 1582 | result.push_str(\"..\");\n 1583 | match self.bounds.1 {\n 1584 | std::ops::Bound::Included(i) => {\n 1585 | result.push('=');\n 1586 | result.push_str(&i.to_string());\n 1587 | }\n 1588 | std::ops::Bound::Excluded(i) => {\n 1589 | result.push_str(&i.to_string());\n 1590 | }\n 1591 | std::ops::Bound::Unbounded => {\n 1592 | result.push_str(&u64::MAX.to_string());\n 1593 | }\n 1594 | }\n 1595 | result\n 1596 | }\n 1597 | }\n 1598 | \n 1599 | impl<T: TryFrom<u64> + Clone + Send + Sync + 'static> TypedValueParser for RangedU64ValueParser<T>\n 1600 | where\n 1601 | <T as TryFrom<u64>>::Error: Send + Sync + 'static + std::error::Error + ToString,\n 1602 | {\n 1603 | type Value = T;\n 1604 | \n 1605 | fn parse_ref(\n 1606 | &self,\n 1607 | cmd: &crate::Command,\n 1608 | arg: Option<&crate::Arg>,\n 1609 | raw_value: &std::ffi::OsStr,\n 1610 | ) -> Result<Self::Value, crate::Error> {\n 1611 | let value = ok!(raw_value.to_str().ok_or_else(|| {\n 1612 | crate::Error::invalid_utf8(\n 1613 | cmd,\n 1614 | crate::output::Usage::new(cmd).create_usage_with_title(&[]),\n 1615 | )\n 1616 | }));\n 1617 | let value = ok!(value.parse::<u64>().map_err(|err| {\n 1618 | let arg = arg\n 1619 | .map(|a| a.to_string())\n 1620 | .unwrap_or_else(|| \"...\".to_owned());\n 1621 | crate::Error::value_validation(\n 1622 | arg,\n 1623 | raw_value.to_string_lossy().into_owned(),\n 1624 | err.into(),\n 1625 | )\n 1626 | .with_cmd(cmd)\n 1627 | }));\n 1628 | if !self.bounds.contains(&value) {\n 1629 | let arg = arg\n 1630 | .map(|a| a.to_string())\n 1631 | .unwrap_or_else(|| \"...\".to_owned());\n 1632 | return Err(crate::Error::value_validation(\n 1633 | arg,\n 1634 | raw_value.to_string_lossy().into_owned(),\n 1635 | format!(\"{} is not in {}\", value, self.format_bounds()).into(),\n 1636 | )\n 1637 | .with_cmd(cmd));\n 1638 | }\n 1639 | \n 1640 | let value: Result<Self::Value, _> = value.try_into();\n 1641 | let value = ok!(value.map_err(|err| {\n 1642 | let arg = arg\n 1643 | .map(|a| a.to_string())\n 1644 | .unwrap_or_else(|| \"...\".to_owned());\n 1645 | crate::Error::value_validation(\n 1646 | arg,\n 1647 | raw_value.to_string_lossy().into_owned(),\n 1648 | err.into(),\n 1649 | )\n 1650 | .with_cmd(cmd)\n 1651 | }));\n 1652 | \n 1653 | Ok(value)\n 1654 | }\n 1655 | }","lines":137}]},{"id":"DUP_3","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","easy":false,"description":"The `write_opts_of` and `write_flags_of` functions in `zsh.rs` both call `arg_conflicts`, format help strings with `escape_help`, compute the `multiple` prefix, and build zsh argument spec strings in near-identical ways. They differ only in value-completion handling (opts have `vc`, flags do not). The shared logic is approximately 30+ lines duplicated.","fix":"Extract a shared helper that computes the common parts (conflicts, multiple prefix, help string) and let `write_opts_of` and `write_flags_of` call it, passing in the value-completion portion as an additional argument.","locations":[{"ref":"clap_complete/src/aot/shells/zsh.rs:457-506","code":" 457 | fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String {\n 458 | debug!(\"write_opts_of\");\n 459 | \n 460 | let mut ret = vec![];\n 461 | \n 462 | for o in p.get_opts() {\n 463 | debug!(\"write_opts_of:iter: o={}\", o.get_id());\n 464 | \n 465 | let help = escape_help(&o.get_help().unwrap_or_default().to_string());\n 466 | let conflicts = arg_conflicts(p, o, p_global);\n 467 | \n 468 | let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() {\n 469 | \"*\"\n 470 | } else {\n 471 | \"\"\n 472 | };\n 473 | \n 474 | let vn = match o.get_value_names() {\n 475 | None => \" \".to_owned(),\n 476 | Some(val) => val[0].to_string(),\n 477 | };\n 478 | let vc = match value_completion(o) {\n 479 | Some(val) => format!(\":{vn}:{val}\"),\n 480 | None => format!(\":{vn}: \"),\n 481 | };\n 482 | let vc = match o.get_num_args().expect(\"built\").min_values() {\n 483 | 0 => format!(\":{vc}\"),\n 484 | min_value => vc.repeat(min_value),\n 485 | };\n 486 | \n 487 | if let Some(shorts) = o.get_short_and_visible_aliases() {\n 488 | for short in shorts {\n 489 | let s = format!(\"'{conflicts}{multiple}-{short}+[{help}]{vc}' \\\\\");\n 490 | \n 491 | debug!(\"write_opts_of:iter: Wrote...{}\", &*s);\n 492 | ret.push(s);\n 493 | }\n 494 | }\n 495 | if let Some(longs) = o.get_long_and_visible_aliases() {\n 496 | for long in longs {\n 497 | let l = format!(\"'{conflicts}{multiple}--{long}=[{help}]{vc}' \\\\\");\n 498 | \n 499 | debug!(\"write_opts_of:iter: Wrote...{}\", &*l);\n 500 | ret.push(l);\n 501 | }\n 502 | }\n 503 | }\n 504 | \n 505 | ret.join(\"\\n\")\n 506 | }","lines":50},{"ref":"clap_complete/src/aot/shells/zsh.rs:546-601","code":" 546 | fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {\n 547 | debug!(\"write_flags_of;\");\n 548 | \n 549 | let mut ret = vec![];\n 550 | \n 551 | for f in utils::flags(p) {\n 552 | debug!(\"write_flags_of:iter: f={}\", f.get_id());\n 553 | \n 554 | let help = escape_help(&f.get_help().unwrap_or_default().to_string());\n 555 | let conflicts = arg_conflicts(p, &f, p_global);\n 556 | \n 557 | let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {\n 558 | \"*\"\n 559 | } else {\n 560 | \"\"\n 561 | };\n 562 | \n 563 | if let Some(short) = f.get_short() {\n 564 | let s = format!(\"'{conflicts}{multiple}-{short}[{help}]' \\\\\");\n 565 | \n 566 | debug!(\"write_flags_of:iter: Wrote...{}\", &*s);\n 567 | \n 568 | ret.push(s);\n 569 | \n 570 | if let Some(short_aliases) = f.get_visible_short_aliases() {\n 571 | for alias in short_aliases {\n 572 | let s = format!(\"'{conflicts}{multiple}-{alias}[{help}]' \\\\\",);\n 573 | \n 574 | debug!(\"write_flags_of:iter: Wrote...{}\", &*s);\n 575 | \n 576 | ret.push(s);\n 577 | }\n 578 | }\n 579 | }\n 580 | \n 581 | if let Some(long) = f.get_long() {\n 582 | let l = format!(\"'{conflicts}{multiple}--{long}[{help}]' \\\\\");\n 583 | \n 584 | debug!(\"write_flags_of:iter: Wrote...{}\", &*l);\n 585 | \n 586 | ret.push(l);\n 587 | \n 588 | if let Some(aliases) = f.get_visible_aliases() {\n 589 | for alias in aliases {\n 590 | let l = format!(\"'{conflicts}{multiple}--{alias}[{help}]' \\\\\");\n 591 | \n 592 | debug!(\"write_flags_of:iter: Wrote...{}\", &*l);\n 593 | \n 594 | ret.push(l);\n 595 | }\n 596 | }\n 597 | }\n 598 | }\n 599 | \n 600 | ret.join(\"\\n\")\n 601 | }","lines":56}]},{"id":"DUP_4","type":"duplicate_code","type_label":"Duplicate Code","severity":"low","easy":false,"description":"The large `#[allow(...)]` attribute block that suppresses clippy and compiler\nwarnings on the generated `impl` blocks is duplicated verbatim in every derive\nmacro output function:\n- `args::gen_for_struct` (args.rs lines 89-108 and 132-151)\n- `subcommand::gen_for_enum` (subcommand.rs lines 69-88 and 103-122)\n- `into_app::gen_for_struct` and `gen_for_enum` (into_app.rs lines 32-51 and 80-99)\n- `value_enum::gen_for_enum` (value_enum.rs lines 53-72)\n\nEach occurrence is an identical 22-line block. This is a macro-level\nduplication of at least 25 lines repeated 6+ times.","fix":"Extract the repeated allow-attribute list into a shared `quote!` helper, e.g.:\n\n fn allow_attrs() -> proc_macro2::TokenStream {\n quote! {\n #[allow(dead_code, unreachable_code, unused_variables, ...)]\n #[allow(clippy::style, clippy::complexity, ...)]\n }\n }\n\nThen call `allow_attrs()` at each emit site instead of repeating the block.","locations":[{"ref":"clap_derive/src/derives/args.rs:89-151","code":" 89 | #[allow(\n 90 | dead_code,\n 91 | unreachable_code,\n 92 | unused_variables,\n 93 | unused_braces,\n 94 | unused_qualifications,\n 95 | )]\n 96 | #[allow(\n 97 | clippy::style,\n 98 | clippy::complexity,\n 99 | clippy::pedantic,\n 100 | clippy::restriction,\n 101 | clippy::perf,\n 102 | clippy::deprecated,\n 103 | clippy::nursery,\n 104 | clippy::cargo,\n 105 | clippy::suspicious_else_formatting,\n 106 | clippy::almost_swapped,\n 107 | clippy::redundant_locals,\n 108 | )]\n 109 | #[automatically_derived]\n 110 | impl #impl_generics clap::FromArgMatches for #item_name #ty_generics #where_clause {\n 111 | fn from_arg_matches(__clap_arg_matches: &clap::ArgMatches) -> ::std::result::Result<Self, clap::Error> {\n 112 | Self::from_arg_matches_mut(&mut __clap_arg_matches.clone())\n 113 | }\n 114 | \n 115 | fn from_arg_matches_mut(__clap_arg_matches: &mut clap::ArgMatches) -> ::std::result::Result<Self, clap::Error> {\n 116 | #raw_deprecated\n 117 | let v = #item_name #constructor;\n 118 | ::std::result::Result::Ok(v)\n 119 | }\n 120 | \n 121 | fn update_from_arg_matches(&mut self, __clap_arg_matches: &clap::ArgMatches) -> ::std::result::Result<(), clap::Error> {\n 122 | self.update_from_arg_matches_mut(&mut __clap_arg_matches.clone())\n 123 | }\n 124 | \n 125 | fn update_from_arg_matches_mut(&mut self, __clap_arg_matches: &mut clap::ArgMatches) -> ::std::result::Result<(), clap::Error> {\n 126 | #raw_deprecated\n 127 | #updater\n 128 | ::std::result::Result::Ok(())\n 129 | }\n 130 | }\n 131 | \n 132 | #[allow(\n 133 | dead_code,\n 134 | unreachable_code,\n 135 | unused_variables,\n 136 | unused_braces,\n 137 | unused_qualifications,\n 138 | )]\n 139 | #[allow(\n 140 | clippy::style,\n 141 | clippy::complexity,\n 142 | clippy::pedantic,\n 143 | clippy::restriction,\n 144 | clippy::perf,\n 145 | clippy::deprecated,\n 146 | clippy::nursery,\n 147 | clippy::cargo,\n 148 | clippy::suspicious_else_formatting,\n 149 | clippy::almost_swapped,\n 150 | clippy::redundant_locals,\n 151 | )]","lines":63},{"ref":"clap_derive/src/derives/subcommand.rs:69-122","code":" 69 | #[allow(\n 70 | dead_code,\n 71 | unreachable_code,\n 72 | unused_variables,\n 73 | unused_braces,\n 74 | unused_qualifications,\n 75 | )]\n 76 | #[allow(\n 77 | clippy::style,\n 78 | clippy::complexity,\n 79 | clippy::pedantic,\n 80 | clippy::restriction,\n 81 | clippy::perf,\n 82 | clippy::deprecated,\n 83 | clippy::nursery,\n 84 | clippy::cargo,\n 85 | clippy::suspicious_else_formatting,\n 86 | clippy::almost_swapped,\n 87 | clippy::redundant_locals,\n 88 | )]\n 89 | #[automatically_derived]\n 90 | impl #impl_generics clap::FromArgMatches for #item_name #ty_generics #where_clause {\n 91 | fn from_arg_matches(__clap_arg_matches: &clap::ArgMatches) -> ::std::result::Result<Self, clap::Error> {\n 92 | Self::from_arg_matches_mut(&mut __clap_arg_matches.clone())\n 93 | }\n 94 | \n 95 | #from_arg_matches\n 96 | \n 97 | fn update_from_arg_matches(&mut self, __clap_arg_matches: &clap::ArgMatches) -> ::std::result::Result<(), clap::Error> {\n 98 | self.update_from_arg_matches_mut(&mut __clap_arg_matches.clone())\n 99 | }\n 100 | #update_from_arg_matches\n 101 | }\n 102 | \n 103 | #[allow(\n 104 | dead_code,\n 105 | unreachable_code,\n 106 | unused_variables,\n 107 | unused_braces,\n 108 | unused_qualifications,\n 109 | )]\n 110 | #[allow(\n 111 | clippy::style,\n 112 | clippy::complexity,\n 113 | clippy::pedantic,\n 114 | clippy::restriction,\n 115 | clippy::perf,\n 116 | clippy::deprecated,\n 117 | clippy::nursery,\n 118 | clippy::cargo,\n 119 | clippy::suspicious_else_formatting,\n 120 | clippy::almost_swapped,\n 121 | clippy::redundant_locals,\n 122 | )]","lines":54},{"ref":"clap_derive/src/derives/into_app.rs:32-99","code":" 32 | #[allow(\n 33 | dead_code,\n 34 | unreachable_code,\n 35 | unused_variables,\n 36 | unused_braces,\n 37 | unused_qualifications,\n 38 | )]\n 39 | #[allow(\n 40 | clippy::style,\n 41 | clippy::complexity,\n 42 | clippy::pedantic,\n 43 | clippy::restriction,\n 44 | clippy::perf,\n 45 | clippy::deprecated,\n 46 | clippy::nursery,\n 47 | clippy::cargo,\n 48 | clippy::suspicious_else_formatting,\n 49 | clippy::almost_swapped,\n 50 | clippy::redundant_locals,\n 51 | )]\n 52 | #[automatically_derived]\n 53 | impl #impl_generics clap::CommandFactory for #item_name #ty_generics #where_clause {\n 54 | fn command<'b>() -> clap::Command {\n 55 | let #app_var = clap::Command::new(#name);\n 56 | <Self as clap::Args>::augment_args(#app_var)\n 57 | }\n 58 | \n 59 | fn command_for_update<'b>() -> clap::Command {\n 60 | let #app_var = clap::Command::new(#name);\n 61 | <Self as clap::Args>::augment_args_for_update(#app_var)\n 62 | }\n 63 | }\n 64 | };\n 65 | \n 66 | Ok(tokens)\n 67 | }\n 68 | \n 69 | pub(crate) fn gen_for_enum(\n 70 | item: &Item,\n 71 | item_name: &Ident,\n 72 | generics: &Generics,\n 73 | ) -> Result<TokenStream, syn::Error> {\n 74 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();\n 75 | \n 76 | let name = item.cased_name();\n 77 | let app_var = Ident::new(\"__clap_app\", Span::call_site());\n 78 | \n 79 | Ok(quote! {\n 80 | #[allow(\n 81 | dead_code,\n 82 | unreachable_code,\n 83 | unused_variables,\n 84 | unused_braces,\n 85 | unused_qualifications,\n 86 | )]\n 87 | #[allow(\n 88 | clippy::style,\n 89 | clippy::complexity,\n 90 | clippy::pedantic,\n 91 | clippy::restriction,\n 92 | clippy::perf,\n 93 | clippy::deprecated,\n 94 | clippy::nursery,\n 95 | clippy::cargo,\n 96 | clippy::suspicious_else_formatting,\n 97 | clippy::almost_swapped,\n 98 | clippy::redundant_locals,\n 99 | )]","lines":68},{"ref":"clap_derive/src/derives/value_enum.rs:53-72","code":" 53 | Ok(quote! {\n 54 | #[allow(\n 55 | dead_code,\n 56 | unreachable_code,\n 57 | unused_variables,\n 58 | unused_braces,\n 59 | unused_qualifications,\n 60 | )]\n 61 | #[allow(\n 62 | clippy::style,\n 63 | clippy::complexity,\n 64 | clippy::pedantic,\n 65 | clippy::restriction,\n 66 | clippy::perf,\n 67 | clippy::deprecated,\n 68 | clippy::nursery,\n 69 | clippy::cargo,\n 70 | clippy::suspicious_else_formatting,\n 71 | clippy::almost_swapped,\n 72 | clippy::redundant_locals,","lines":20}]},{"id":"ARCH_1","type":"architecture","type_label":"Architecture","severity":"medium","easy":false,"description":"AppSettings is a #[repr(u8)] enum stored as a u32 bitfield (AppFlags). There are currently exactly 32 variants, meaning bit 31 is already occupied. Adding even one more AppSettings variant would silently compute 1u32 << 32 which in Rust (in debug builds) panics due to shift overflow, or wraps to 0 in release builds, silently aliasing the new variant to bit 0 and breaking all flags built from that point on. There is no static guard preventing this.","fix":"Add a compile-time assertion (e.g. const _: () = assert!(AppSettings::BinNameBuilt as u8 < 32, \"AppSettings exceeds u32 bitfield capacity\");) or change AppFlags to u64 to give room for growth, and update the bit() fn accordingly.","locations":[{"ref":"clap_builder/src/builder/app_settings.rs:48-82","code":" 48 | pub(crate) enum AppSettings {\n 49 | IgnoreErrors,\n 50 | AllowHyphenValues,\n 51 | AllowNegativeNumbers,\n 52 | AllArgsOverrideSelf,\n 53 | AllowMissingPositional,\n 54 | TrailingVarArg,\n 55 | DontDelimitTrailingValues,\n 56 | InferLongArgs,\n 57 | InferSubcommands,\n 58 | SubcommandRequired,\n 59 | AllowExternalSubcommands,\n 60 | Multicall,\n 61 | SubcommandsNegateReqs,\n 62 | ArgsNegateSubcommands,\n 63 | SubcommandPrecedenceOverArg,\n 64 | FlattenHelp,\n 65 | ArgRequiredElseHelp,\n 66 | NextLineHelp,\n 67 | DisableColoredHelp,\n 68 | DisableHelpFlag,\n 69 | DisableHelpSubcommand,\n 70 | DisableVersionFlag,\n 71 | PropagateVersion,\n 72 | Hidden,\n 73 | HidePossibleValues,\n 74 | HelpExpected,\n 75 | NoBinaryName,\n 76 | #[allow(dead_code)]\n 77 | ColorAuto,\n 78 | ColorAlways,\n 79 | ColorNever,\n 80 | Built,\n 81 | BinNameBuilt,\n 82 | }","lines":35},{"ref":"clap_builder/src/builder/app_settings.rs:84-88","code":" 84 | impl AppSettings {\n 85 | fn bit(self) -> u32 {\n 86 | 1 << (self as u8)\n 87 | }\n 88 | }","lines":5}]},{"id":"ARCH_2","type":"architecture","type_label":"Architecture","severity":"low","easy":false,"description":"The `ArgMatcher::propagate_globals` method only calls `fill_in_global_values` but the method name in the debug log says 'ArgMatcher::get_global_values' (line 48 in arg_matcher.rs). The log message is misleading and refers to a stale function name.","fix":"Update the debug log message to say 'ArgMatcher::propagate_globals' to match the actual function name.","locations":[{"ref":"clap_builder/src/parser/arg_matcher.rs:47-51","code":" 47 | pub(crate) fn propagate_globals(&mut self, global_arg_vec: &[Id]) {\n 48 | debug!(\"ArgMatcher::get_global_values: global_arg_vec={global_arg_vec:?}\");\n 49 | let mut vals_map = FlatMap::new();\n 50 | self.fill_in_global_values(global_arg_vec, &mut vals_map);\n 51 | }","lines":5}]},{"id":"ARCH_3","type":"architecture","type_label":"Architecture","severity":"low","easy":false,"description":"The `MatchedArg::vals` and `MatchedArg::raw_vals` fields are kept in lockstep (both `Vec<Vec<...>>`), and every operation (new_val_group, append_val) modifies both in parallel. If any code path adds to one but not the other, they become desynchronized and panics or type errors will result downstream. There is no structural enforcement of the lockstep invariant.","fix":"Encapsulate both vecs into a single `ValPair { val: Vec<AnyValue>, raw: OsString }` and store `Vec<Vec<ValPair>>` so the lockstep is structural rather than relying on discipline.","locations":[{"ref":"clap_builder/src/parser/matches/matched_arg.rs:16-23","code":" 16 | pub(crate) struct MatchedArg {\n 17 | source: Option<ValueSource>,\n 18 | indices: Vec<usize>,\n 19 | type_id: Option<AnyValueId>,\n 20 | vals: Vec<Vec<AnyValue>>,\n 21 | raw_vals: Vec<Vec<OsString>>,\n 22 | ignore_case: bool,\n 23 | }","lines":8},{"ref":"clap_builder/src/parser/matches/matched_arg.rs:111-123","code":" 111 | pub(crate) fn new_val_group(&mut self) {\n 112 | self.vals.push(vec![]);\n 113 | self.raw_vals.push(vec![]);\n 114 | }\n 115 | \n 116 | pub(crate) fn append_val(&mut self, val: AnyValue, raw_val: OsString) {\n 117 | // We assume there is always a group created before.\n 118 | self.vals.last_mut().expect(INTERNAL_ERROR_MSG).push(val);\n 119 | self.raw_vals\n 120 | .last_mut()\n 121 | .expect(INTERNAL_ERROR_MSG)\n 122 | .push(raw_val);\n 123 | }","lines":13}]},{"id":"OTHER_1","type":"other","type_label":"Other","severity":"low","easy":true,"description":"`_copy_subtree_for_help()` checks `get_about().is_some()` and then immediately calls\n`get_about().unwrap()` on the next line. The double-access forces two separate option evaluations\nwhere one is sufficient, and is an antipattern that can silently break if the interior value ever\nbecomes computed rather than stored. Should use `if let Some(about) = self.get_about()`.","fix":"Replace the `if self.get_about().is_some() { ... .get_about().unwrap() }` pattern with\n`if let Some(about) = self.get_about() { ... about }`.","locations":[{"ref":"clap_builder/src/builder/command.rs:4882-4892","code":" 4882 | fn _copy_subtree_for_help(&self) -> Command {\n 4883 | let mut cmd = Command::new(self.name.clone())\n 4884 | .hide(self.is_hide_set())\n 4885 | .global_setting(AppSettings::DisableHelpFlag)\n 4886 | .global_setting(AppSettings::DisableVersionFlag)\n 4887 | .subcommands(self.get_subcommands().map(Command::_copy_subtree_for_help));\n 4888 | if self.get_about().is_some() {\n 4889 | cmd = cmd.about(self.get_about().unwrap().clone());\n 4890 | }\n 4891 | cmd\n 4892 | }","lines":11}]}]</script>
<script type="application/json" id="PI">{"name":"clap","description":"","git_url":"https://github.com/clap-rs/clap.git","repo_url":"https://github.com/clap-rs/clap","commit":"8387c812c4b9726474b8407f81ec97f0acf570ed","commit_short":"8387c812c4","commit_url":"https://github.com/clap-rs/clap/commit/8387c812c4b9726474b8407f81ec97f0acf570ed","branch":"master","generated":"2026-06-14"}</script>
<div class="app">
<div class="top">
<div>
<h1>Code Audit Report — clap</h1>
<div class="meta">Generated 2026-06-14 — <span id="stats"></span></div>
</div>
<div class="top-right">
<input type="search" id="search" placeholder="Search findings…" autocomplete="off">
<label class="group-label">Group by
<select id="groupby">
<option value="type">Type</option>
<option value="severity">Severity</option>
<option value="file">File</option>
<option value="none">None (flat)</option>
</select>
</label>
</div>
</div>
<div class="project-info"><div class="pi-name">clap</div><div class="pi-row"><a class="pi-repo-btn" href="https://github.com/clap-rs/clap" target="_blank" rel="noopener">🔗 https://github.com/clap-rs/clap</a><span><span class="pi-k">branch</span> <code>master</code></span><span><span class="pi-k">commit</span> <a href="https://github.com/clap-rs/clap/commit/8387c812c4b9726474b8407f81ec97f0acf570ed" target="_blank" rel="noopener"><code>8387c812c4</code></a></span></div></div>
<div class="filterbar" id="filterbar"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<span id="progress-text"></span>
<button class="action-btn" data-action="collapse-all">⊟ Collapse all</button>
<button class="action-btn" data-action="expand-all">⊞ Expand all</button>
<button class="action-btn" data-action="download-md">⬇ MD</button>
<button class="action-btn" data-action="download-md-short">⬇ MD Short</button>
<button class="action-btn" data-action="download-toml">⬇ TOML</button>
</div>
<div id="hidden-bar"></div>
<div id="content"></div>
</div>
<script>
(function(){
'use strict';
// Data
const FINDINGS = JSON.parse(document.getElementById('D').textContent);
const PROJECT_INFO = JSON.parse(document.getElementById('PI').textContent);
const SEVS = ['critical','high','medium','low','negligible'];
const STATUSES = ['open','fixed','wont-fix','false-positive'];
const ALL_TYPES = [...new Set(FINDINGS.map(f => f.type))].sort();
// Build lookup map for O(1) access
const FINDING_BY_ID = new Map(FINDINGS.map(f => [f.id, f]));
// Only severities actually present in the report — the filter bar hides the rest.
const PRESENT_SEVS = SEVS.filter(s => FINDINGS.some(f => (f.severity || 'medium') === s));
// Whether any finding is flagged easy — the "Quick wins" filter row is hidden otherwise.
const HAS_EASY = FINDINGS.some(f => f.easy);
const STORE_KEY = 'audit-status:' + location.href;
const STATUS_CLS = {open:'st-open', fixed:'st-fixed', 'wont-fix':'st-wont', 'false-positive':'st-fp'};
const STATUS_LBL = {open:'● open', fixed:'✓ fixed', 'wont-fix':"— won't fix", 'false-positive':'✗ false pos.'};
const STATUS_NEXT = {open:'fixed', fixed:'wont-fix', 'wont-fix':'false-positive', 'false-positive':'open'};
// State
const S = {
search: '', groupBy: 'type',
hiddenSev: new Set(), hiddenType: new Set(),
hiddenStatus: new Set(), hiddenGroup: new Set(),
collapsed: new Set(), easyOnly: false,
};
const statuses = {};
// Persistence
function loadStatuses() {
try { Object.assign(statuses, JSON.parse(localStorage.getItem(STORE_KEY) || '{}')); } catch {}
}
function saveStatuses() {
try { localStorage.setItem(STORE_KEY, JSON.stringify(statuses)); } catch {}
}
function statusOf(id) { return statuses[id] || 'open'; }
// URL hash — debounced so rapid filter changes don't thrash history
let _hashTimer = 0;
function syncHash() {
clearTimeout(_hashTimer);
_hashTimer = setTimeout(() => {
const p = new URLSearchParams();
if (S.groupBy !== 'type') p.set('g', S.groupBy);
if (S.hiddenSev.size) p.set('hs', [...S.hiddenSev].join(','));
if (S.hiddenType.size) p.set('ht', [...S.hiddenType].join(','));
if (S.hiddenStatus.size) p.set('hst', [...S.hiddenStatus].join(','));
if (S.easyOnly) p.set('e', '1');
if (S.search) p.set('q', S.search);
const str = p.toString();
history.replaceState(null, '', str ? '#' + str : location.pathname + location.search);
}, 300);
}
function loadHash() {
const p = new URLSearchParams(location.hash.slice(1));
if (p.has('g')) { S.groupBy = p.get('g'); document.getElementById('groupby').value = S.groupBy; }
if (p.has('q')) { S.search = p.get('q'); document.getElementById('search').value = S.search; }
if (p.has('hs')) p.get('hs') .split(',').filter(Boolean).forEach(v => S.hiddenSev.add(v));
if (p.has('ht')) p.get('ht') .split(',').filter(Boolean).forEach(v => S.hiddenType.add(v));
if (p.has('hst')) p.get('hst').split(',').filter(Boolean).forEach(v => S.hiddenStatus.add(v));
if (p.has('e')) S.easyOnly = p.get('e') === '1';
}
// Helpers
function esc(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function highlight(text, q) {
if (!q) return esc(text);
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + ')', 'gi');
return String(text).split(re).map((p, i) => i % 2 ? '<mark>' + esc(p) + '</mark>' : esc(p)).join('');
}
// Render text with ```fenced``` code blocks and `inline` backticks,
// also paragraph-splitting on blank lines. Output is HTML-safe.
function renderMd(text, q) {
const src = String(text == null ? '' : text);
const FENCE = /```[ \t]*([A-Za-z0-9_+-]*)[ \t]*\r?\n([\s\S]*?)```/g;
const parts = [];
let last = 0, m;
while ((m = FENCE.exec(src)) !== null) {
parts.push({kind: 'text', value: src.slice(last, m.index)});
parts.push({kind: 'code', value: m[2].replace(/\r?\n$/, '')});
last = m.index + m[0].length;
}
parts.push({kind: 'text', value: src.slice(last)});
function renderInline(s) {
const re = /`([^`\n]+)`/g;
let out = '', i = 0, mm;
while ((mm = re.exec(s)) !== null) {
out += highlight(s.slice(i, mm.index), q);
out += '<code class="md-inline">' + highlight(mm[1], q) + '</code>';
i = mm.index + mm[0].length;
}
out += highlight(s.slice(i), q);
return out;
}
function renderTextChunk(s) {
const paragraphs = s.split(/\r?\n[ \t]*\r?\n/);
const out = [];
for (const p of paragraphs) {
const trimmed = p.replace(/^\s+|\s+$/g, '');
if (!trimmed) continue;
out.push('<p>' + renderInline(trimmed).replace(/\r?\n/g, '<br>') + '</p>');
}
return out.join('');
}
let html = '';
for (const p of parts) {
if (p.kind === 'text') html += renderTextChunk(p.value);
else html += '<pre class="md-code">' + highlight(p.value, q) + '</pre>';
}
return html;
}
function sevCls(s) { return SEVS.includes(s) ? 'sev-'+s : 'sev-negligible'; }
function sevLbl(s) { return s ? s[0].toUpperCase()+s.slice(1) : 'Medium'; }
function typeLabel(t) { return t.replace(/_/g,' ').replace(/\b\w/g, c => c.toUpperCase()); }
// First sentence of text (up to and including the first period).
function firstSentence(text) {
const m = String(text).match(/\.(\s|$)/);
return m ? text.slice(0, text.indexOf(m[0]) + 1) : text;
}
// Build full MD report (mirrors to_markdown in Python).
function buildMdFull() {
const SORD = ['critical','high','medium','low','negligible'];
const today = new Date().toISOString().slice(0, 10);
let out = '# Code Audit Report\n\n_Generated ' + today + '_\n\n';
const typeMap = new Map();
for (const f of FINDINGS) {
if (!typeMap.has(f.type)) typeMap.set(f.type, []);
typeMap.get(f.type).push(f);
}
const sortedTypes = [...typeMap.keys()].sort();
out += '## Summary\n\n| Type | Severity | Count |\n|------|----------|-------|\n';
for (const t of sortedTypes) {
const items = typeMap.get(t);
const topSev = items.map(f => f.severity || 'medium').reduce((best, s) => {
const bi = SORD.indexOf(best), si = SORD.indexOf(s);
return (si >= 0 && (bi < 0 || si < bi)) ? s : best;
}, 'negligible');
out += '| ' + typeLabel(t) + ' | ' + topSev + ' | ' + items.length + ' |\n';
}
for (const t of sortedTypes) {
out += '\n## ' + typeLabel(t) + '\n\n';
for (const f of typeMap.get(t)) {
const sevStr = f.severity ? ' `' + f.severity.toUpperCase() + '`' : '';
const easyStr = f.easy ? ' ⚡ easy' : '';
out += '### ' + f.id + sevStr + easyStr + '\n\n';
out += '**Description:** ' + (f.description || '') + '\n\n';
if (f.locations.length) {
out += '**Locations:**\n\n';
for (const l of f.locations) {
if (l.code != null) out += '```\n' + l.ref + '\n' + l.code + '\n```\n\n';
else out += '- `' + l.ref + '` _(file not found)_\n\n';
}
}
out += '**Fix:** ' + (f.fix || '') + '\n\n---\n\n';
}
}
return out;
}
// Build MD short checklist (mirrors to_md_short in Python).
function buildMdShort() {
const lines = [];
const ids = [];
const sorted = [...FINDINGS].sort((a, b) => a.type.localeCompare(b.type));
for (const f of sorted) {
ids.push(f.id);
const desc = firstSentence(f.description || '');
const locs = f.locations.map(l => l.ref);
const locStr = locs.length ? ' - [' + locs.join(', ') + ']' : '';
const sevStr = f.severity ? ' [' + f.severity.toUpperCase() + ']' : '';
const easyStr = f.easy ? ' ⚡' : '';
lines.push('- [ ] - ' + f.id + sevStr + easyStr + ' - ' + desc + locStr);
}
lines.push('', '## IDs only', '');
ids.forEach(id => lines.push('- [ ] - ' + id));
return lines.join('\n') + '\n';
}
function downloadText(text, filename, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], {type: mime || 'text/plain'}));
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}
function tomlStr(s) {
return '"' + String(s == null ? '' : s)
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t') + '"';
}
// Build TOML matching the original findings.toml format (re-parseable by toml_to_report.py).
function buildFindingsToml() {
function pad(key) { return (key + ' ').slice(0, 12); }
const chunks = [];
for (const f of FINDINGS) {
const lns = ['[[finding]]'];
lns.push(pad('type') + '= ' + tomlStr(f.type));
lns.push(pad('severity') + '= ' + tomlStr(f.severity || 'medium'));
if (f.easy) lns.push(pad('easy') + '= true');
lns.push(pad('description') + '= ' + tomlStr(f.description || ''));
const refs = f.locations.map(l => tomlStr(l.ref));
lns.push(pad('locations') + '= [' + refs.join(', ') + ']');
lns.push(pad('fix') + '= ' + tomlStr(f.fix || ''));
chunks.push(lns.join('\n'));
}
return chunks.join('\n\n') + '\n';
}
// Build project-info.toml from embedded PROJECT_INFO (same format as gen_project_info.py).
function buildProjectInfoToml() {
const ORDER = ['name','description','git_url','repo_url','commit','commit_short','commit_url','branch','generated'];
const pi = Object.assign({}, PROJECT_INFO);
if (!('description' in pi)) pi.description = '';
const lines = [
'# Project metadata for the code audit report (all fields optional).',
'# Hand-edit freely (e.g. fill in description); re-run the audit without --force to keep edits.',
'', '[project]'
];
for (const k of ORDER) {
if (k in pi) lines.push(k + ' = ' + tomlStr(pi[k]));
}
return lines.join('\n') + '\n';
}
// Build the GitHub-ready Markdown for one finding (mirrors findings.md).
function findingToMd(f) {
let out = '### ' + f.id + (f.severity ? ' `' + f.severity.toUpperCase() + '`' : '') + (f.easy ? ' ⚡ easy' : '') + '\n\n';
out += '**Description:** ' + (f.description || '') + '\n\n';
if (f.locations.length) {
out += '**Locations:**\n\n';
for (const l of f.locations) {
if (l.code != null) out += '```\n' + l.ref + '\n' + l.code + '\n```\n\n';
else out += '- `' + l.ref + '` _(file not found)_\n\n';
}
}
out += '**Fix:** ' + (f.fix || '') + '\n';
return out;
}
function flashBtn(btn, msg) {
btn.textContent = msg;
setTimeout(() => { btn.textContent = '⧉ Copy'; }, 1200);
}
function copyText(text, btn) {
const ok = () => flashBtn(btn, '✓ Copied');
const no = () => flashBtn(btn, '✗ Failed');
const fallback = () => {
try {
const ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.select();
const r = document.execCommand('copy');
document.body.removeChild(ta);
r ? ok() : no();
} catch { no(); }
};
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(text).then(ok, fallback);
else fallback();
}
// Filter logic
function getKey(f) {
if (S.groupBy === 'severity') return f.severity || 'medium';
if (S.groupBy === 'file') return f.locations.length ? f.locations[0].ref.replace(/:.*/,'') : '(no location)';
if (S.groupBy === 'none') return '';
return f.type_label;
}
// Pre-compute search haystack per finding (cleared on full re-render)
const _hay = new Map();
function hayOf(f) {
if (!_hay.has(f.id)) {
_hay.set(f.id, [f.id, f.type_label, f.severity, f.description, f.fix,
f.easy ? 'easy quick win' : '',
...f.locations.map(l => l.ref)].join(' ').toLowerCase());
}
return _hay.get(f.id);
}
function matches(f) {
if (S.easyOnly && !f.easy) return false;
if (S.hiddenSev.has(f.severity || 'medium')) return false;
if (S.hiddenType.has(f.type)) return false;
const st = statusOf(f.id);
if (S.hiddenStatus.has(st)) return false;
if (S.search) {
const q = S.search.toLowerCase();
if (!hayOf(f).includes(q) && !st.includes(q)) return false;
}
return true;
}
function sortKeys(keys) {
if (S.groupBy === 'severity') {
return [...keys].sort((a, b) => {
const ai = SEVS.indexOf(a), bi = SEVS.indexOf(b);
return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
});
}
return [...keys].sort();
}
// Build body HTML for an expanded card (called lazily on first expand).
function renderCardBody(f, q) {
let locs = '';
for (const l of f.locations) {
const lineInfo = l.lines != null
? ' <span class="loc-lines">(' + l.lines + ' line' + (l.lines === 1 ? '' : 's') + ')</span>'
: '';
locs += '<div class="loc-block"><div class="loc-ref">' + highlight(l.ref, q) + lineInfo + '</div>';
locs += l.code != null ? '<pre>' + esc(l.code) + '</pre>' : '<div class="not-found">(file not found)</div>';
locs += '</div>';
}
return (
'<div class="field-lbl">Description</div>' +
'<div class="desc">' + renderMd(f.description, q) + '</div>' +
(locs ? '<div class="field-lbl">Locations</div>' + locs : '') +
'<div class="field-lbl">Fix</div>' +
'<div class="fix-txt">' + renderMd(f.fix, q) + '</div>'
);
}
// Render card shell only — header + empty or populated body.
// When collapsed the body is empty (display:none) — avoids renderMd for 1800+ cards.
function renderCard(f, q) {
const sc = sevCls(f.severity || 'medium');
const sl = sevLbl(f.severity || 'medium');
const st = statusOf(f.id);
const sc2 = STATUS_CLS[st] || 'st-open';
const sl2 = STATUS_LBL[st] || '● open';
const isCollapsed = S.collapsed.has(f.id);
const isResolved = st !== 'open';
const locSummary = f.locations.length
? f.locations[0].ref + (f.locations.length > 1 ? ' \u2026' : '')
: '';
const bodyInner = isCollapsed ? '' : renderCardBody(f, q);
return (
'<div class="card' + (isResolved ? ' resolved' : '') + '" data-card-id="' + esc(f.id) + '">' +
'<div class="card-header" data-action="toggle-card" data-id="' + esc(f.id) + '">' +
'<span class="card-arrow">' + (isCollapsed ? '\u25b6' : '\u25bc') + '</span>' +
'<span class="card-id">' + esc(f.id) + '</span>' +
'<span class="badge ' + sc + '" style="cursor:default">' + esc(sl) + '</span>' +
'<span class="badge type-badge" style="cursor:default">' + esc(f.type_label) + '</span>' +
(f.easy ? '<span class="badge easy-badge" style="cursor:default" title="Easy to verify & fix - no deep knowledge of the code, SDK or technology required">\u26a1 Easy</span>' : '') +
(locSummary ? '<span class="card-loc" title="' + esc(locSummary) + '">' + esc(locSummary) + '</span>' : '') +
'<button class="status-btn ' + sc2 + '" data-action="cycle-status" data-id="' + esc(f.id) + '">' + sl2 + '</button>' +
'<button class="copy-btn" data-action="copy-md" data-id="' + esc(f.id) + '" title="Copy as Markdown for GitHub">\u29c9 Copy</button>' +
'</div>' +
'<div class="card-body" data-body-id="' + esc(f.id) + '"' + (isCollapsed ? ' style="display:none"' : '') + '>' +
bodyInner +
'</div></div>'
);
}
function initFilterBar() {
const fb = document.getElementById('filterbar');
let h = '<div class="filter-row"><span class="filter-lbl">Severity:</span>';
for (const s of PRESENT_SEVS)
h += '<button class="badge ' + sevCls(s) + '" data-sev="' + s + '">' + sevLbl(s) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="sev"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Type:</span>';
for (const t of ALL_TYPES)
h += '<button class="badge type-badge" data-type="' + esc(t) + '">' + esc(typeLabel(t)) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="type"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Status:</span>';
for (const st of STATUSES)
h += '<button class="badge ' + (STATUS_CLS[st]||'st-open') + '" data-status="' + st + '">' + (STATUS_LBL[st]||st) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="status"></button></div>';
if (HAS_EASY)
h += '<div class="filter-row"><span class="filter-lbl">Quick wins:</span>'
+ '<button class="badge easy-badge" data-easy="1">\u26a1 Easy only</button></div>';
fb.innerHTML = h;
}
function updateToggles() {
for (const b of document.querySelectorAll('[data-sev]')) b.classList.toggle('off', S.hiddenSev.has(b.dataset.sev));
for (const b of document.querySelectorAll('[data-type]')) b.classList.toggle('off', S.hiddenType.has(b.dataset.type));
for (const b of document.querySelectorAll('[data-status]')) b.classList.toggle('off', S.hiddenStatus.has(b.dataset.status));
for (const b of document.querySelectorAll('[data-easy]')) b.classList.toggle('off', !S.easyOnly);
for (const b of document.querySelectorAll('[data-toggle-all]')) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : S.hiddenStatus;
b.textContent = set.size === 0 ? 'Hide all' : 'Show all';
}
}
function updateProgress() {
const total = FINDINGS.length;
const done = FINDINGS.filter(f => statusOf(f.id) !== 'open').length;
const pct = total ? Math.round(done / total * 100) : 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-text').textContent = done + ' / ' + total + ' resolved (' + pct + '%)';
}
// Surgical toggle: flip one card without touching the rest of the DOM.
function toggleCard(id) {
const wasCollapsed = S.collapsed.has(id);
if (wasCollapsed) S.collapsed.delete(id); else S.collapsed.add(id);
const body = document.querySelector('[data-body-id="' + id + '"]');
const arrow = document.querySelector('[data-card-id="' + id + '"] .card-arrow');
if (!body) { render(); return; }
if (wasCollapsed) {
if (!body.firstChild) {
const f = FINDING_BY_ID.get(id);
if (f) body.innerHTML = renderCardBody(f, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
} else {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
}
}
// Surgical status update: update one card's button + resolved class.
function cycleStatus(id) {
statuses[id] = STATUS_NEXT[statusOf(id)] || 'open';
saveStatuses();
const st = statusOf(id);
const btn = document.querySelector('.status-btn[data-id="' + id + '"]');
if (!btn) { render(); return; }
btn.className = 'status-btn ' + (STATUS_CLS[st] || 'st-open');
btn.textContent = STATUS_LBL[st] || '\u25cf open';
const card = document.querySelector('[data-card-id="' + id + '"]');
if (card) card.classList.toggle('resolved', st !== 'open');
updateProgress();
}
// Surgical collapse/expand for a group: toggle all bodies in the group without full re-render.
function collapseGroup(key, collapse) {
FINDINGS.forEach(f => {
if (getKey(f) !== key) return;
if (collapse) S.collapsed.add(f.id); else S.collapsed.delete(f.id);
const body = document.querySelector('[data-body-id="' + f.id + '"]');
const arrow = document.querySelector('[data-card-id="' + f.id + '"] .card-arrow');
if (!body) return;
if (collapse) {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
} else {
if (!body.firstChild) {
const ff = FINDING_BY_ID.get(f.id);
if (ff) body.innerHTML = renderCardBody(ff, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
}
});
}
// Full re-render: used for filter/group-by/search changes.
// Batched via requestAnimationFrame.
let _rafPending = false;
function scheduleRender() {
if (_rafPending) return;
_rafPending = true;
requestAnimationFrame(() => { _rafPending = false; render(); });
}
function render() {
_hay.clear();
updateToggles();
updateProgress();
const q = S.search;
const visible = FINDINGS.filter(matches);
document.getElementById('stats').textContent = visible.length + ' / ' + FINDINGS.length + ' findings';
// Build groups
const map = new Map();
for (const f of visible) {
const k = getKey(f);
if (!map.has(k)) map.set(k, []);
map.get(k).push(f);
}
// Hidden-groups bar
const hbar = document.getElementById('hidden-bar');
if (S.hiddenGroup.size > 0) {
let hh = '<div class="hidden-inner"><span class="hidden-lbl">Hidden:</span>';
for (const g of [...S.hiddenGroup].sort())
hh += '<button class="show-btn" data-action="show-group" data-key="' + esc(g) + '">' + esc(g || '(all findings)') + '</button>';
hh += '<button class="show-all-btn" data-action="show-all">Show all</button></div>';
hbar.innerHTML = hh;
} else {
hbar.innerHTML = '';
}
// Content
const content = document.getElementById('content');
if (!visible.length) {
content.innerHTML = '<div class="no-results">No findings match the current filters.</div>';
syncHash();
return;
}
let hh = '';
for (const k of sortKeys([...map.keys()])) {
if (S.hiddenGroup.has(k)) continue;
const items = map.get(k);
const title = k || 'All Findings';
const done = items.filter(f => statusOf(f.id) !== 'open').length;
const allDone = done === items.length;
hh += '<div class="group-section">' +
'<div class="group-header">' +
'<span class="group-title' + (allDone ? ' all-done' : '') + '">' + esc(title) + '</span>' +
'<span class="group-count">' + done + '/' + items.length + '</span>' +
'<button class="icon-btn" data-action="collapse-group" data-key="' + esc(k) + '" title="Collapse all">\u229f</button>' +
'<button class="icon-btn" data-action="expand-group" data-key="' + esc(k) + '" title="Expand all">\u229e</button>' +
'<button class="hide-btn" data-action="hide-group" data-key="' + esc(k) + '">Hide</button>' +
'</div>';
for (const f of items) hh += renderCard(f, q);
hh += '</div>';
}
content.innerHTML = hh;
syncHash();
}
// Event delegation
document.getElementById('filterbar').addEventListener('click', e => {
const b = e.target.closest('[data-sev],[data-type],[data-status],[data-toggle-all],[data-easy]');
if (!b) return;
if (b.dataset.easy) {
S.easyOnly = !S.easyOnly;
}
else if (b.dataset.toggleAll) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : S.hiddenStatus;
const full = k === 'sev' ? PRESENT_SEVS : k === 'type' ? ALL_TYPES : STATUSES;
if (set.size === 0) full.forEach(v => set.add(v));
else set.clear();
}
else if (b.dataset.sev) { const s = S.hiddenSev; s.has(b.dataset.sev) ? s.delete(b.dataset.sev) : s.add(b.dataset.sev); }
else if (b.dataset.type) { const s = S.hiddenType; s.has(b.dataset.type) ? s.delete(b.dataset.type) : s.add(b.dataset.type); }
else if (b.dataset.status) { const s = S.hiddenStatus; s.has(b.dataset.status) ? s.delete(b.dataset.status) : s.add(b.dataset.status); }
scheduleRender();
});
document.addEventListener('click', e => {
const b = e.target.closest('[data-action]');
if (!b) return;
const { action, key, id } = b.dataset;
if (action === 'hide-group') { S.hiddenGroup.add(key); scheduleRender(); }
else if (action === 'show-group') { S.hiddenGroup.delete(key); scheduleRender(); }
else if (action === 'show-all') { S.hiddenGroup.clear(); scheduleRender(); }
else if (action === 'collapse-all') { FINDINGS.forEach(f => S.collapsed.add(f.id)); scheduleRender(); }
else if (action === 'expand-all') { S.collapsed.clear(); scheduleRender(); }
else if (action === 'copy-md') {
e.stopPropagation();
const f = FINDING_BY_ID.get(id);
if (f) copyText(findingToMd(f), b);
}
else if (action === 'download-md') { downloadText(buildMdFull(), 'findings.md'); }
else if (action === 'download-md-short') { downloadText(buildMdShort(), 'findings-short.md'); }
else if (action === 'download-toml') {
downloadText(buildFindingsToml(), 'findings.toml', 'application/toml');
downloadText(buildProjectInfoToml(), 'project-info.toml', 'application/toml');
}
else if (action === 'cycle-status') {
e.stopPropagation();
cycleStatus(id);
}
else if (action === 'toggle-card') {
toggleCard(id);
}
else if (action === 'collapse-group') {
collapseGroup(key, true);
}
else if (action === 'expand-group') {
collapseGroup(key, false);
}
});
// Debounced search — 180 ms after last keystroke
let _searchTimer = 0;
document.getElementById('search').addEventListener('input', e => {
S.search = e.target.value;
clearTimeout(_searchTimer);
_searchTimer = setTimeout(scheduleRender, 180);
});
document.getElementById('groupby').addEventListener('change', e => {
S.groupBy = e.target.value;
S.hiddenGroup.clear();
scheduleRender();
});
// Init
loadStatuses();
initFilterBar();
loadHash();
FINDINGS.forEach(f => S.collapsed.add(f.id));
render();
})();
</script>
</body></html>