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