1use std::{fmt, str::FromStr};
14
15use super::Code;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub enum RepairSafety {
28 FormatOnly,
31 BehaviorPreserving,
34 ScopeLocal,
38 SurfaceChanging,
41 CapabilityChanging,
45 NeedsHuman,
49}
50
51impl RepairSafety {
52 pub const ALL: &'static [RepairSafety] = &[
53 RepairSafety::FormatOnly,
54 RepairSafety::BehaviorPreserving,
55 RepairSafety::ScopeLocal,
56 RepairSafety::SurfaceChanging,
57 RepairSafety::CapabilityChanging,
58 RepairSafety::NeedsHuman,
59 ];
60
61 pub const fn as_str(self) -> &'static str {
65 match self {
66 RepairSafety::FormatOnly => "format-only",
67 RepairSafety::BehaviorPreserving => "behavior-preserving",
68 RepairSafety::ScopeLocal => "scope-local",
69 RepairSafety::SurfaceChanging => "surface-changing",
70 RepairSafety::CapabilityChanging => "capability-changing",
71 RepairSafety::NeedsHuman => "needs-human",
72 }
73 }
74
75 pub const fn is_at_most(self, ceiling: RepairSafety) -> bool {
79 (self as u8) <= (ceiling as u8)
80 }
81
82 pub const fn is_machine_applicable(self) -> bool {
85 self.is_at_most(RepairSafety::BehaviorPreserving)
86 }
87}
88
89impl fmt::Display for RepairSafety {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct ParseRepairSafetyError;
98
99impl fmt::Display for ParseRepairSafetyError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str("unknown Harn repair-safety class")
102 }
103}
104
105impl std::error::Error for ParseRepairSafetyError {}
106
107impl FromStr for RepairSafety {
108 type Err = ParseRepairSafetyError;
109
110 fn from_str(value: &str) -> Result<Self, Self::Err> {
111 RepairSafety::ALL
112 .iter()
113 .copied()
114 .find(|safety| safety.as_str() == value)
115 .ok_or(ParseRepairSafetyError)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub struct RepairId(std::borrow::Cow<'static, str>);
126
127impl RepairId {
128 pub const fn from_static(s: &'static str) -> Self {
129 RepairId(std::borrow::Cow::Borrowed(s))
130 }
131
132 pub fn from_owned(s: String) -> Self {
133 RepairId(std::borrow::Cow::Owned(s))
134 }
135
136 pub fn as_str(&self) -> &str {
137 &self.0
138 }
139}
140
141impl fmt::Display for RepairId {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.write_str(&self.0)
144 }
145}
146
147#[derive(Debug, Clone)]
155pub struct Repair {
156 pub id: RepairId,
157 pub summary: String,
158 pub safety: RepairSafety,
159}
160
161impl Repair {
162 pub fn from_template(template: &RepairTemplate) -> Self {
163 Repair {
164 id: RepairId::from_static(template.id),
165 summary: template.summary.to_string(),
166 safety: template.safety,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy)]
178pub struct RepairTemplate {
179 pub id: &'static str,
180 pub summary: &'static str,
181 pub safety: RepairSafety,
182}
183
184impl Code {
185 pub const fn repair_template(self) -> Option<&'static RepairTemplate> {
188 match self {
189 Code::TypeMismatch
191 | Code::ReturnTypeMismatch
192 | Code::AssignmentTypeMismatch
193 | Code::ArgumentTypeMismatch
194 | Code::VariableTypeMismatch
195 | Code::ClosureReturnTypeMismatch
196 | Code::FieldTypeMismatch
197 | Code::MethodTypeMismatch
198 | Code::InvalidIndexType => Some(&REPAIR_INSERT_EXPLICIT_CONVERSION),
199 Code::StringInterpolationRewrite => Some(&REPAIR_REWRITE_STRING_INTERPOLATION),
200 Code::UnknownTypeName => Some(&REPAIR_IMPORTS_FIX_PATH),
201 Code::ImplicitAnyParameter => Some(&REPAIR_TYPES_ANNOTATE_PARAMETER),
202 Code::InvalidCast => Some(&REPAIR_CASTS_REMOVE_UNCHECKED),
203
204 Code::UndefinedVariable
206 | Code::UndefinedFunction
207 | Code::UnknownField
208 | Code::UnknownMethod
209 | Code::UnknownBuiltin
210 | Code::UnknownDeclaration => Some(&REPAIR_BINDINGS_RENAME_TO_CLOSEST),
211 Code::InvalidMainSignature => Some(&REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM),
212 Code::DeprecatedFunction => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
213 Code::RemovedBuiltinMethod => Some(&REPAIR_METHODS_MIGRATE_REMOVED),
214 Code::ModuleImportUnresolved | Code::ImportResolutionFailed => {
215 Some(&REPAIR_IMPORTS_FIX_PATH)
216 }
217 Code::ModuleImportUnused => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
218 Code::ModuleImportOrder => Some(&REPAIR_IMPORTS_REORDER),
219
220 Code::CapabilityResultUnchecked => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
222 Code::CapabilityBindingInvalid => Some(&REPAIR_MANUAL_REVIEW_CAPABILITY),
223 Code::EffectInheritanceViolation => Some(&REPAIR_POLICY_NARROW_CHILD_EFFECTS),
224 Code::RescueOutsideFunction | Code::TryOutsideFunction => {
225 Some(&REPAIR_ERRORS_WRAP_IN_FN)
226 }
227
228 Code::LlmSchemaMissing => Some(&REPAIR_LLM_ADD_SCHEMA),
230 Code::LlmProviderIdentityBranch | Code::PromptProviderIdentityBranch => {
231 Some(&REPAIR_LLM_USE_CAPABILITY_FLAG)
232 }
233 Code::PromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
234 Code::PromptToolSurfaceUnknown | Code::PromptToolSurfaceDeferredReference => {
235 Some(&REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE)
236 }
237 Code::PromptVariantExplosion => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
238
239 Code::DeprecatedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
241 Code::LintMissingStdlibMetadata => Some(&REPAIR_DOC_ADD_STDLIB_METADATA),
242
243 Code::ImmutableAssignment => Some(&REPAIR_BINDINGS_MAKE_MUTABLE),
245 Code::MutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
246
247 Code::NonExhaustiveMatch => Some(&REPAIR_MATCH_ADD_MISSING_ARMS),
249 Code::DuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
250
251 Code::UnreachableCode => Some(&REPAIR_DEAD_CODE_REMOVE),
253
254 Code::FormatterWouldReformat | Code::FormatterTrailingComma => {
256 Some(&REPAIR_FORMAT_REFORMAT)
257 }
258
259 Code::LintUnusedVariable
261 | Code::LintUnusedPatternBinding
262 | Code::LintUnusedParameter => Some(&REPAIR_BINDINGS_RENAME_UNUSED),
263 Code::LintUnusedPipelineInput => Some(&REPAIR_BINDINGS_REMOVE_UNUSED_PIPELINE_INPUT),
264 Code::LintCapabilityParameterName => Some(&REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER),
265 Code::LintUnusedImport => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
266 Code::LintUnusedFunction | Code::LintUnusedType => {
267 Some(&REPAIR_DECLARATIONS_REMOVE_UNUSED)
268 }
269 Code::LintMutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
270 Code::LintImportOrder => Some(&REPAIR_IMPORTS_REORDER),
271 Code::LintBlankLineBetweenItems
272 | Code::LintTrailingComma
273 | Code::LintUnnecessaryParentheses
274 | Code::LintRequireFileHeader => Some(&REPAIR_FORMAT_REFORMAT),
275 Code::LintLegacyDocComment => Some(&REPAIR_DOC_COMMENT_MIGRATE),
276 Code::LintEmptyBlock => Some(&REPAIR_BLOCK_REMOVE_EMPTY),
277 Code::LintUnnecessaryElseReturn | Code::LintLetThenReturn => {
278 Some(&REPAIR_CONTROL_FLOW_FLATTEN)
279 }
280 Code::LintNilCoalesceNoop
281 | Code::LintNilCoalesceSelfFallback
282 | Code::LintRedundantNilTernary
283 | Code::LintUnnecessarySafeNavigation
284 | Code::LintUnnecessaryNonNullAssert
285 | Code::LintPreferOptionalShorthand
286 | Code::LintComparisonToBool
287 | Code::LintPointlessComparison
288 | Code::LintConstantLogicalOperand => Some(&REPAIR_EXPRESSION_SIMPLIFY),
289 Code::LintPreferPick => Some(&REPAIR_RECORDS_PICK_FIELDS),
290 Code::LintUnnecessaryCast => Some(&REPAIR_CASTS_REMOVE_REDUNDANT),
291 Code::LintRedundantClone => Some(&REPAIR_CLONE_REMOVE_REDUNDANT),
292 Code::LintEagerCollectionConversion => Some(&REPAIR_COLLECTION_PREFER_LAZY),
293 Code::LintDeadCodeAfterReturn => Some(&REPAIR_DEAD_CODE_REMOVE),
294 Code::LintRenamedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
295 Code::LintAmbientClockBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_CLOCK),
296 Code::LintAmbientFsBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_FS),
297 Code::LintAmbientEnvBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_ENV),
298 Code::LintAmbientRandomBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_RANDOM),
299 Code::LintAmbientNetBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_NET),
300 Code::LintAmbientStdioBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS),
301 Code::LintAmbientHarnessMethod => Some(&REPAIR_BINDINGS_THREAD_HARNESS_METHOD),
302 Code::LintBroadHarnessParameter => Some(&REPAIR_BINDINGS_ATTENUATE_HARNESS),
303 Code::LintRemovedLlmOptions => Some(&REPAIR_LLM_MIGRATE_REMOVED_OPTION),
304 Code::LintTemplateProviderIdentityBranch => Some(&REPAIR_LLM_USE_CAPABILITY_FLAG),
305 Code::LintPromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
306 Code::LintShadowVariable => Some(&REPAIR_BINDINGS_RENAME_SHADOW),
307 Code::LintNamingConvention => Some(&REPAIR_STYLE_RENAME_TO_CONVENTION),
308 Code::LintUnhandledApprovalResult => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
309 Code::LintMissingHarndoc => Some(&REPAIR_DOC_ADD_HARNDOC),
310 Code::LintDuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
311 Code::LintUntypedDictAccess => Some(&REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE),
317 Code::LintUnnormalizedOptions => Some(&REPAIR_TYPES_ADD_SHAPE_ANNOTATION),
318 Code::LintMcpToolAnnotations => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
319 Code::LintTemplateVariantExplosion | Code::LintLongRunningWithoutCleanup => {
320 Some(&REPAIR_MANUAL_NEEDS_HUMAN)
321 }
322
323 _ => None,
327 }
328 }
329}
330
331const REPAIR_INSERT_EXPLICIT_CONVERSION: RepairTemplate = RepairTemplate {
346 id: "casts/insert-explicit-conversion",
347 summary: "Insert an explicit conversion or correct the operand type",
348 safety: RepairSafety::ScopeLocal,
349};
350
351const REPAIR_REWRITE_STRING_INTERPOLATION: RepairTemplate = RepairTemplate {
352 id: "style/string-interpolation",
353 summary: "Rewrite string concatenation as an interpolation literal",
354 safety: RepairSafety::BehaviorPreserving,
355};
356
357const REPAIR_CASTS_REMOVE_UNCHECKED: RepairTemplate = RepairTemplate {
358 id: "casts/remove-unchecked",
359 summary: "Remove the unchecked cast or guard it with a type test",
360 safety: RepairSafety::ScopeLocal,
361};
362
363const REPAIR_CASTS_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
364 id: "casts/remove-redundant",
365 summary: "Remove the redundant cast",
366 safety: RepairSafety::BehaviorPreserving,
367};
368
369const REPAIR_BINDINGS_RENAME_TO_CLOSEST: RepairTemplate = RepairTemplate {
370 id: "bindings/rename-to-closest",
371 summary: "Rename to the closest in-scope identifier",
372 safety: RepairSafety::ScopeLocal,
373};
374
375const REPAIR_BINDINGS_MAKE_MUTABLE: RepairTemplate = RepairTemplate {
376 id: "bindings/make-mutable",
377 summary: "Declare the binding with `let` so it can be reassigned",
378 safety: RepairSafety::ScopeLocal,
379};
380
381const REPAIR_BINDINGS_MAKE_IMMUTABLE: RepairTemplate = RepairTemplate {
382 id: "bindings/make-immutable",
383 summary: "Declare the never-reassigned binding with `const` instead of `let`",
384 safety: RepairSafety::BehaviorPreserving,
385};
386
387const REPAIR_BINDINGS_RENAME_UNUSED: RepairTemplate = RepairTemplate {
388 id: "bindings/rename-unused",
389 summary: "Mark an unused binding without changing callable arity",
390 safety: RepairSafety::BehaviorPreserving,
391};
392
393const REPAIR_BINDINGS_REMOVE_UNUSED_PIPELINE_INPUT: RepairTemplate = RepairTemplate {
394 id: "bindings/remove-unused-pipeline-input",
395 summary: "Remove an explicitly unused test pipeline input",
396 safety: RepairSafety::SurfaceChanging,
397};
398
399const REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER: RepairTemplate = RepairTemplate {
400 id: "bindings/name-capability-parameter",
401 summary: "Rename the capability parameter and its references after the capability it carries",
402 safety: RepairSafety::SurfaceChanging,
403};
404
405const REPAIR_BINDINGS_RENAME_SHADOW: RepairTemplate = RepairTemplate {
406 id: "bindings/rename-shadow",
407 summary: "Rename the shadowing binding to a distinct name",
408 safety: RepairSafety::ScopeLocal,
409};
410
411const REPAIR_BINDINGS_THREAD_HARNESS: RepairTemplate = RepairTemplate {
412 id: "bindings/thread-harness",
413 summary: "Thread the existing `harness` binding through local helper calls and replace the ambient stdio builtin with `harness.stdio.*`",
414 safety: RepairSafety::ScopeLocal,
415};
416
417const REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM: RepairTemplate = RepairTemplate {
418 id: "bindings/thread-harness-needs-param",
419 summary: "Add a `harness: Harness` parameter where the stdio capability handle is required and update local callers",
420 safety: RepairSafety::SurfaceChanging,
421};
422
423const REPAIR_BINDINGS_THREAD_HARNESS_METHOD: RepairTemplate = RepairTemplate {
424 id: "bindings/thread-harness-method",
425 summary: "Replace the ambient runtime builtin with its typed `harness.*` method and thread authority through local callers",
426 safety: RepairSafety::ScopeLocal,
427};
428
429const REPAIR_BINDINGS_THREAD_HARNESS_CLOCK: RepairTemplate = RepairTemplate {
430 id: "bindings/thread-harness-clock",
431 summary: "Replace the ambient clock builtin with the corresponding `harness.clock.*` method",
432 safety: RepairSafety::ScopeLocal,
433};
434
435const REPAIR_BINDINGS_THREAD_HARNESS_FS: RepairTemplate = RepairTemplate {
436 id: "bindings/thread-harness-fs",
437 summary: "Replace the ambient fs builtin with the corresponding `harness.fs.*` method",
438 safety: RepairSafety::ScopeLocal,
439};
440
441const REPAIR_BINDINGS_THREAD_HARNESS_ENV: RepairTemplate = RepairTemplate {
442 id: "bindings/thread-harness-env",
443 summary: "Replace the ambient env builtin with the corresponding `harness.env.*` method",
444 safety: RepairSafety::ScopeLocal,
445};
446
447const REPAIR_BINDINGS_THREAD_HARNESS_RANDOM: RepairTemplate = RepairTemplate {
448 id: "bindings/thread-harness-random",
449 summary: "Replace the ambient random builtin with the corresponding `harness.random.*` method",
450 safety: RepairSafety::ScopeLocal,
451};
452
453const REPAIR_BINDINGS_THREAD_HARNESS_NET: RepairTemplate = RepairTemplate {
454 id: "bindings/thread-harness-net",
455 summary: "Replace the ambient net builtin with the corresponding `harness.net.*` method",
456 safety: RepairSafety::ScopeLocal,
457};
458
459const REPAIR_DECLARATIONS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
460 id: "declarations/remove-unused",
461 summary: "Remove the unused declaration",
462 safety: RepairSafety::SurfaceChanging,
463};
464
465const REPAIR_IMPORTS_FIX_PATH: RepairTemplate = RepairTemplate {
466 id: "imports/fix-path",
467 summary: "Replace the import path with a resolvable target",
468 safety: RepairSafety::ScopeLocal,
469};
470
471const REPAIR_IMPORTS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
472 id: "imports/remove-unused",
473 summary: "Remove the unused import",
474 safety: RepairSafety::BehaviorPreserving,
475};
476
477const REPAIR_IMPORTS_REORDER: RepairTemplate = RepairTemplate {
478 id: "imports/reorder",
479 summary: "Reorder imports into canonical grouping",
480 safety: RepairSafety::FormatOnly,
481};
482
483const REPAIR_ERRORS_CHECK_OR_RESCUE: RepairTemplate = RepairTemplate {
484 id: "errors/check-or-rescue",
485 summary: "Check the result or wrap the call in a `rescue` block",
486 safety: RepairSafety::ScopeLocal,
487};
488
489const REPAIR_ERRORS_WRAP_IN_FN: RepairTemplate = RepairTemplate {
490 id: "errors/wrap-in-fn",
491 summary: "Move the construct inside a function body",
492 safety: RepairSafety::SurfaceChanging,
493};
494
495const REPAIR_MATCH_ADD_MISSING_ARMS: RepairTemplate = RepairTemplate {
496 id: "match/add-missing-arms",
497 summary: "Add arms covering the missing variants",
498 safety: RepairSafety::ScopeLocal,
499};
500
501const REPAIR_MATCH_REMOVE_DUPLICATE_ARM: RepairTemplate = RepairTemplate {
502 id: "match/remove-duplicate-arm",
503 summary: "Remove the duplicated match arm",
504 safety: RepairSafety::BehaviorPreserving,
505};
506
507const REPAIR_FORMAT_REFORMAT: RepairTemplate = RepairTemplate {
508 id: "format/reformat",
509 summary: "Apply canonical formatting",
510 safety: RepairSafety::FormatOnly,
511};
512
513const REPAIR_DOC_COMMENT_MIGRATE: RepairTemplate = RepairTemplate {
514 id: "doc/migrate-comment-style",
515 summary: "Migrate the legacy comment to canonical doc syntax",
516 safety: RepairSafety::FormatOnly,
517};
518
519const REPAIR_DOC_ADD_HARNDOC: RepairTemplate = RepairTemplate {
520 id: "doc/add-harndoc",
521 summary: "Add a `///` doc comment describing this declaration",
522 safety: RepairSafety::BehaviorPreserving,
523};
524
525const REPAIR_DOC_ADD_STDLIB_METADATA: RepairTemplate = RepairTemplate {
526 id: "doc/add-stdlib-metadata",
527 summary: "Add `@effects` and `@errors` fields to the stdlib function's doc block",
528 safety: RepairSafety::BehaviorPreserving,
529};
530
531const REPAIR_BLOCK_REMOVE_EMPTY: RepairTemplate = RepairTemplate {
532 id: "blocks/remove-empty",
533 summary: "Remove the empty block or fill in an explicit body",
534 safety: RepairSafety::ScopeLocal,
535};
536
537const REPAIR_CONTROL_FLOW_FLATTEN: RepairTemplate = RepairTemplate {
538 id: "control-flow/flatten",
539 summary: "Flatten the unnecessary control flow construct",
540 safety: RepairSafety::BehaviorPreserving,
541};
542
543const REPAIR_EXPRESSION_SIMPLIFY: RepairTemplate = RepairTemplate {
544 id: "expressions/simplify",
545 summary: "Simplify the expression to its canonical form",
546 safety: RepairSafety::BehaviorPreserving,
547};
548
549const REPAIR_RECORDS_PICK_FIELDS: RepairTemplate = RepairTemplate {
550 id: "records/pick-fields",
551 summary: "Replace the field-by-field record copy with `pick`",
552 safety: RepairSafety::BehaviorPreserving,
553};
554
555const REPAIR_CLONE_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
556 id: "clones/remove-redundant",
557 summary: "Remove the redundant clone",
558 safety: RepairSafety::BehaviorPreserving,
559};
560
561const REPAIR_COLLECTION_PREFER_LAZY: RepairTemplate = RepairTemplate {
562 id: "collections/prefer-lazy",
563 summary: "Replace the eager collection step with a lazy variant",
564 safety: RepairSafety::ScopeLocal,
565};
566
567const REPAIR_DEAD_CODE_REMOVE: RepairTemplate = RepairTemplate {
568 id: "control-flow/remove-dead",
569 summary: "Remove the unreachable code",
570 safety: RepairSafety::BehaviorPreserving,
571};
572
573const REPAIR_METHODS_MIGRATE_REMOVED: RepairTemplate = RepairTemplate {
574 id: "methods/migrate-removed",
575 summary: "Rewrite the removed builtin method spelling to its canonical replacement",
576 safety: RepairSafety::BehaviorPreserving,
577};
578
579const REPAIR_STDLIB_MIGRATE_RENAMED: RepairTemplate = RepairTemplate {
580 id: "stdlib/migrate-renamed",
581 summary: "Rename the call to the renamed stdlib symbol",
582 safety: RepairSafety::ScopeLocal,
583};
584
585const REPAIR_BINDINGS_ATTENUATE_HARNESS: RepairTemplate = RepairTemplate {
586 id: "bindings/attenuate-harness",
587 summary: "Replace the root Harness parameter with the single capability the helper uses",
588 safety: RepairSafety::SurfaceChanging,
589};
590
591const REPAIR_LLM_MIGRATE_REMOVED_OPTION: RepairTemplate = RepairTemplate {
592 id: "llm/migrate-removed-option",
593 summary: "Replace the removed option with its supported equivalent",
594 safety: RepairSafety::ScopeLocal,
595};
596
597const REPAIR_LLM_ADD_SCHEMA: RepairTemplate = RepairTemplate {
598 id: "llm/add-schema",
599 summary: "Add a typed output schema to the LLM call",
600 safety: RepairSafety::SurfaceChanging,
601};
602
603const REPAIR_LLM_USE_CAPABILITY_FLAG: RepairTemplate = RepairTemplate {
604 id: "llm/use-capability-flag",
605 summary: "Branch on a capability flag instead of provider identity",
606 safety: RepairSafety::CapabilityChanging,
607};
608
609const REPAIR_PROMPTS_ESCAPE_INJECTION: RepairTemplate = RepairTemplate {
610 id: "prompts/escape-injection",
611 summary: "Pass the untrusted input through a structured placeholder",
612 safety: RepairSafety::ScopeLocal,
613};
614
615const REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE: RepairTemplate = RepairTemplate {
616 id: "prompts/add-tool-to-surface",
617 summary: "Add the referenced tool to the declared tool surface",
618 safety: RepairSafety::SurfaceChanging,
619};
620
621const REPAIR_STYLE_RENAME_TO_CONVENTION: RepairTemplate = RepairTemplate {
622 id: "style/rename-to-convention",
623 summary: "Rename to match the casing convention for this kind",
624 safety: RepairSafety::SurfaceChanging,
625};
626
627const REPAIR_TYPES_ADD_SHAPE_ANNOTATION: RepairTemplate = RepairTemplate {
628 id: "types/add-shape-annotation",
629 summary: "Annotate the dict with a concrete shape type",
630 safety: RepairSafety::SurfaceChanging,
631};
632
633const REPAIR_TYPES_ANNOTATE_PARAMETER: RepairTemplate = RepairTemplate {
643 id: "types/annotate-parameter",
644 summary: "Annotate the parameter with the inferred type, or `unknown` and narrow it at the dynamic boundary",
645 safety: RepairSafety::SurfaceChanging,
646};
647
648const REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE: RepairTemplate = RepairTemplate {
663 id: "types/validate-boundary-value",
664 summary: "Validate the parsed value with schema_expect() or schema_check() before reading it",
665 safety: RepairSafety::ScopeLocal,
666};
667
668const REPAIR_MANUAL_REVIEW_CAPABILITY: RepairTemplate = RepairTemplate {
669 id: "manual/review-capability-binding",
670 summary: "Review the capability binding; the fix is not mechanical",
671 safety: RepairSafety::NeedsHuman,
672};
673
674const REPAIR_POLICY_NARROW_CHILD_EFFECTS: RepairTemplate = RepairTemplate {
675 id: "policy/narrow-child-effects",
676 summary: "Narrow the child agent's effects to a subset of the parent's, or widen the parent's declared effects",
677 safety: RepairSafety::SurfaceChanging,
678};
679
680const REPAIR_MANUAL_NEEDS_HUMAN: RepairTemplate = RepairTemplate {
681 id: "manual/needs-human",
682 summary: "Plan a human-led change; auto-apply is not safe here",
683 safety: RepairSafety::NeedsHuman,
684};
685
686pub const REPAIR_REGISTRY: &[&RepairTemplate] = &[
689 &REPAIR_INSERT_EXPLICIT_CONVERSION,
690 &REPAIR_REWRITE_STRING_INTERPOLATION,
691 &REPAIR_CASTS_REMOVE_UNCHECKED,
692 &REPAIR_CASTS_REMOVE_REDUNDANT,
693 &REPAIR_BINDINGS_RENAME_TO_CLOSEST,
694 &REPAIR_BINDINGS_MAKE_MUTABLE,
695 &REPAIR_BINDINGS_MAKE_IMMUTABLE,
696 &REPAIR_BINDINGS_RENAME_UNUSED,
697 &REPAIR_BINDINGS_REMOVE_UNUSED_PIPELINE_INPUT,
698 &REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER,
699 &REPAIR_BINDINGS_RENAME_SHADOW,
700 &REPAIR_BINDINGS_THREAD_HARNESS,
701 &REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM,
702 &REPAIR_BINDINGS_THREAD_HARNESS_METHOD,
703 &REPAIR_BINDINGS_THREAD_HARNESS_CLOCK,
704 &REPAIR_BINDINGS_THREAD_HARNESS_FS,
705 &REPAIR_BINDINGS_THREAD_HARNESS_ENV,
706 &REPAIR_BINDINGS_THREAD_HARNESS_RANDOM,
707 &REPAIR_BINDINGS_THREAD_HARNESS_NET,
708 &REPAIR_DECLARATIONS_REMOVE_UNUSED,
709 &REPAIR_IMPORTS_FIX_PATH,
710 &REPAIR_IMPORTS_REMOVE_UNUSED,
711 &REPAIR_IMPORTS_REORDER,
712 &REPAIR_ERRORS_CHECK_OR_RESCUE,
713 &REPAIR_ERRORS_WRAP_IN_FN,
714 &REPAIR_MATCH_ADD_MISSING_ARMS,
715 &REPAIR_MATCH_REMOVE_DUPLICATE_ARM,
716 &REPAIR_FORMAT_REFORMAT,
717 &REPAIR_DOC_COMMENT_MIGRATE,
718 &REPAIR_DOC_ADD_HARNDOC,
719 &REPAIR_DOC_ADD_STDLIB_METADATA,
720 &REPAIR_BLOCK_REMOVE_EMPTY,
721 &REPAIR_CONTROL_FLOW_FLATTEN,
722 &REPAIR_EXPRESSION_SIMPLIFY,
723 &REPAIR_RECORDS_PICK_FIELDS,
724 &REPAIR_CLONE_REMOVE_REDUNDANT,
725 &REPAIR_COLLECTION_PREFER_LAZY,
726 &REPAIR_DEAD_CODE_REMOVE,
727 &REPAIR_STDLIB_MIGRATE_RENAMED,
728 &REPAIR_METHODS_MIGRATE_REMOVED,
729 &REPAIR_BINDINGS_ATTENUATE_HARNESS,
730 &REPAIR_LLM_MIGRATE_REMOVED_OPTION,
731 &REPAIR_LLM_ADD_SCHEMA,
732 &REPAIR_LLM_USE_CAPABILITY_FLAG,
733 &REPAIR_PROMPTS_ESCAPE_INJECTION,
734 &REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE,
735 &REPAIR_STYLE_RENAME_TO_CONVENTION,
736 &REPAIR_TYPES_ADD_SHAPE_ANNOTATION,
737 &REPAIR_TYPES_ANNOTATE_PARAMETER,
738 &REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE,
739 &REPAIR_MANUAL_REVIEW_CAPABILITY,
740 &REPAIR_MANUAL_NEEDS_HUMAN,
741 &REPAIR_POLICY_NARROW_CHILD_EFFECTS,
742];
743
744#[cfg(test)]
745mod tests {
746 use super::super::Category;
747 use super::{Code, ParseRepairSafetyError, RepairSafety, REPAIR_REGISTRY};
748 use std::collections::HashSet;
749 use std::str::FromStr;
750
751 #[test]
752 fn parses_registered_code() {
753 assert_eq!(Code::from_str("HARN-TYP-014"), Ok(Code::TypeParameterArity));
754 }
755
756 #[test]
757 fn registry_has_unique_identifiers() {
758 let mut seen = HashSet::new();
759 for entry in Code::registry() {
760 assert!(
761 seen.insert(entry.identifier),
762 "duplicate diagnostic code {}",
763 entry.identifier
764 );
765 assert_eq!(entry.code.as_str(), entry.identifier);
766 assert_eq!(entry.code.category(), entry.category);
767 let expected_prefix = format!("HARN-{}-", entry.category);
768 assert!(entry.identifier.starts_with(&expected_prefix));
769 let suffix = entry.identifier.trim_start_matches(&expected_prefix);
770 assert_eq!(suffix.len(), 3);
771 assert!(suffix.chars().all(|ch| ch.is_ascii_digit()));
772 assert!(!entry.summary.is_empty());
773 }
774 assert!(Code::registry().len() >= 40);
775 }
776
777 #[test]
778 fn every_category_is_populated() {
779 for category in Category::ALL {
780 assert!(
781 Code::registry()
782 .iter()
783 .any(|entry| entry.category == *category),
784 "missing diagnostic code category {category}"
785 );
786 }
787 }
788
789 #[test]
790 fn every_code_has_non_empty_explanation() {
791 for entry in Code::registry() {
792 let body = entry.code.explanation();
793 assert!(
794 !body.trim().is_empty(),
795 "diagnostic code {} has an empty explanation file",
796 entry.identifier
797 );
798 assert!(
799 body.contains(entry.identifier),
800 "explanation for {} should reference its identifier",
801 entry.identifier
802 );
803 }
804 }
805
806 #[test]
807 fn related_codes_are_registered_and_non_self() {
808 for entry in Code::registry() {
809 for &other in entry.code.related() {
810 assert_ne!(
811 other, entry.code,
812 "{} lists itself as a related code",
813 entry.identifier
814 );
815 assert!(
816 Code::registry().iter().any(|e| e.code == other),
817 "{} lists unregistered related code {}",
818 entry.identifier,
819 other
820 );
821 }
822 }
823 }
824
825 #[test]
826 fn repair_safety_string_roundtrip() {
827 for safety in RepairSafety::ALL {
828 let parsed = RepairSafety::from_str(safety.as_str()).unwrap();
829 assert_eq!(parsed, *safety);
830 assert_eq!(parsed.to_string(), safety.as_str());
831 }
832 assert_eq!(
833 RepairSafety::from_str("not-a-safety-class"),
834 Err(ParseRepairSafetyError)
835 );
836 }
837
838 #[test]
839 fn repair_safety_ordering_is_monotonic_low_to_high() {
840 let order = RepairSafety::ALL;
844 for window in order.windows(2) {
845 assert!(
846 window[0] < window[1],
847 "{:?} should be safer than {:?}",
848 window[0],
849 window[1]
850 );
851 assert!(window[0].is_at_most(window[1]));
852 assert!(!window[1].is_at_most(window[0]));
853 }
854 }
855
856 #[test]
857 fn only_behavior_preserving_repairs_are_machine_applicable() {
858 assert!(RepairSafety::FormatOnly.is_machine_applicable());
859 assert!(RepairSafety::BehaviorPreserving.is_machine_applicable());
860 for safety in [
861 RepairSafety::ScopeLocal,
862 RepairSafety::SurfaceChanging,
863 RepairSafety::CapabilityChanging,
864 RepairSafety::NeedsHuman,
865 ] {
866 assert!(!safety.is_machine_applicable(), "{safety}");
867 }
868 }
869
870 #[test]
871 fn repair_registry_has_at_least_twenty_entries() {
872 assert!(
873 REPAIR_REGISTRY.len() >= 20,
874 "expected ≥20 repair templates, found {}",
875 REPAIR_REGISTRY.len()
876 );
877 }
878
879 #[test]
880 fn repair_ids_are_kebab_case_namespaced_and_unique() {
881 let mut seen = HashSet::new();
882 for template in REPAIR_REGISTRY {
883 assert!(
884 seen.insert(template.id),
885 "duplicate repair id {}",
886 template.id
887 );
888 let (namespace, leaf) = template.id.split_once('/').unwrap_or_else(|| {
889 panic!(
890 "repair id `{}` is missing `<namespace>/` prefix",
891 template.id
892 )
893 });
894 assert!(
895 !namespace.is_empty() && !leaf.is_empty(),
896 "repair id `{}` has empty namespace or leaf",
897 template.id
898 );
899 for ch in template.id.chars() {
900 assert!(
901 ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '/',
902 "repair id `{}` has non-kebab character {ch:?}",
903 template.id
904 );
905 }
906 assert!(
907 !template.summary.is_empty(),
908 "repair {} has empty summary",
909 template.id
910 );
911 let first = template.summary.chars().next().unwrap();
913 assert!(
914 first.is_ascii_uppercase(),
915 "repair {} summary `{}` should start with a capital",
916 template.id,
917 template.summary
918 );
919 }
920 }
921
922 #[test]
923 fn manual_namespace_is_needs_human() {
924 for template in REPAIR_REGISTRY {
925 if let Some(("manual", _)) = template.id.split_once('/') {
926 assert_eq!(
927 template.safety,
928 RepairSafety::NeedsHuman,
929 "manual/* repair {} must be NeedsHuman",
930 template.id
931 );
932 }
933 }
934 }
935
936 #[test]
937 fn known_codes_carry_expected_safety_class() {
938 let expected: &[(Code, RepairSafety, &str)] = &[
942 (
943 Code::FormatterWouldReformat,
944 RepairSafety::FormatOnly,
945 "format/reformat",
946 ),
947 (
948 Code::ModuleImportUnused,
949 RepairSafety::BehaviorPreserving,
950 "imports/remove-unused",
951 ),
952 (
953 Code::ImmutableAssignment,
954 RepairSafety::ScopeLocal,
955 "bindings/make-mutable",
956 ),
957 (
958 Code::LintUnusedFunction,
959 RepairSafety::SurfaceChanging,
960 "declarations/remove-unused",
961 ),
962 (
963 Code::LlmProviderIdentityBranch,
964 RepairSafety::CapabilityChanging,
965 "llm/use-capability-flag",
966 ),
967 (
968 Code::PromptVariantExplosion,
969 RepairSafety::NeedsHuman,
970 "manual/needs-human",
971 ),
972 (
973 Code::NonExhaustiveMatch,
974 RepairSafety::ScopeLocal,
975 "match/add-missing-arms",
976 ),
977 (
978 Code::LintAmbientClockBuiltin,
979 RepairSafety::ScopeLocal,
980 "bindings/thread-harness-clock",
981 ),
982 (
983 Code::LintAmbientStdioBuiltin,
984 RepairSafety::ScopeLocal,
985 "bindings/thread-harness",
986 ),
987 (
988 Code::InvalidMainSignature,
989 RepairSafety::SurfaceChanging,
990 "bindings/thread-harness-needs-param",
991 ),
992 ];
993 for (code, safety, repair_id) in expected {
994 let template = code
995 .repair_template()
996 .unwrap_or_else(|| panic!("{code} should have a repair template"));
997 assert_eq!(template.safety, *safety, "{code} safety class drifted");
998 assert_eq!(template.id, *repair_id, "{code} repair id drifted");
999 }
1000 }
1001
1002 #[test]
1003 fn repair_templates_cover_at_least_twenty_codes() {
1004 let covered = Code::ALL
1005 .iter()
1006 .filter(|code| code.repair_template().is_some())
1007 .count();
1008 assert!(
1009 covered >= 20,
1010 "expected ≥20 codes with a repair template, found {covered}"
1011 );
1012 }
1013
1014 #[test]
1028 fn boundary_validation_codes_offer_the_most_informative_repair() {
1029 for code in [Code::BoundaryValueUnvalidated, Code::LintUntypedDictAccess] {
1030 let Some(template) = code.repair_template() else {
1031 continue;
1032 };
1033 assert_ne!(
1034 template.id, "types/add-shape-annotation",
1035 "{code:?} is a boundary-validation rule; an annotation validates the value \
1036 (harn#6252) but reports only the declared type and the value's kind, so the \
1037 offered one-step repair must be the one that names the failing field"
1038 );
1039 }
1040 }
1041
1042 #[test]
1043 fn every_registered_repair_is_referenced_by_some_code() {
1044 let referenced: HashSet<&'static str> = Code::ALL
1045 .iter()
1046 .filter_map(|code| code.repair_template())
1047 .map(|template| template.id)
1048 .collect();
1049 for template in REPAIR_REGISTRY {
1050 assert!(
1051 referenced.contains(template.id),
1052 "repair {} is in REPAIR_REGISTRY but no Code maps to it",
1053 template.id
1054 );
1055 }
1056 }
1057
1058 #[test]
1059 fn every_referenced_repair_template_is_in_registry() {
1060 let registered: HashSet<&'static str> =
1061 REPAIR_REGISTRY.iter().map(|template| template.id).collect();
1062 for code in Code::ALL {
1063 let Some(template) = code.repair_template() else {
1064 continue;
1065 };
1066 assert!(
1067 registered.contains(template.id),
1068 "repair {} (used by {}) is missing from REPAIR_REGISTRY",
1069 template.id,
1070 code
1071 );
1072 }
1073 }
1074}