1use std::fmt;
21use std::str::FromStr;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub enum Category {
26 Typ,
27 Par,
28 Nam,
29 Cap,
30 Llm,
31 Orc,
32 Std,
33 Prm,
34 Mod,
35 Lnt,
36 Fmt,
37 Imp,
38 Own,
39 Rcv,
40 Mat,
41}
42
43impl Category {
44 pub const ALL: &'static [Category] = &[
45 Category::Typ,
46 Category::Par,
47 Category::Nam,
48 Category::Cap,
49 Category::Llm,
50 Category::Orc,
51 Category::Std,
52 Category::Prm,
53 Category::Mod,
54 Category::Lnt,
55 Category::Fmt,
56 Category::Imp,
57 Category::Own,
58 Category::Rcv,
59 Category::Mat,
60 ];
61
62 pub const fn as_str(self) -> &'static str {
63 match self {
64 Category::Typ => "TYP",
65 Category::Par => "PAR",
66 Category::Nam => "NAM",
67 Category::Cap => "CAP",
68 Category::Llm => "LLM",
69 Category::Orc => "ORC",
70 Category::Std => "STD",
71 Category::Prm => "PRM",
72 Category::Mod => "MOD",
73 Category::Lnt => "LNT",
74 Category::Fmt => "FMT",
75 Category::Imp => "IMP",
76 Category::Own => "OWN",
77 Category::Rcv => "RCV",
78 Category::Mat => "MAT",
79 }
80 }
81}
82
83impl fmt::Display for Category {
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 RegistryEntry {
92 pub code: Code,
93 pub identifier: &'static str,
94 pub category: Category,
95 pub summary: &'static str,
96}
97
98macro_rules! diagnostic_codes {
99 ($($variant:ident, $identifier:literal, $category:ident, $summary:literal;)*) => {
100 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
102 pub enum Code {
103 $($variant,)*
104 }
105
106 impl Code {
107 pub const ALL: &'static [Code] = &[
108 $(Code::$variant,)*
109 ];
110
111 pub const fn as_str(self) -> &'static str {
112 match self {
113 $(Code::$variant => $identifier,)*
114 }
115 }
116
117 pub const fn category(self) -> Category {
118 match self {
119 $(Code::$variant => Category::$category,)*
120 }
121 }
122
123 pub const fn summary(self) -> &'static str {
124 match self {
125 $(Code::$variant => $summary,)*
126 }
127 }
128
129 pub const fn explanation(self) -> &'static str {
133 match self {
134 $(Code::$variant => include_str!(
135 concat!("diagnostic_codes/explanations/", $identifier, ".md")
136 ),)*
137 }
138 }
139 }
140
141 pub const REGISTRY: &[RegistryEntry] = &[
142 $(RegistryEntry {
143 code: Code::$variant,
144 identifier: $identifier,
145 category: Category::$category,
146 summary: $summary,
147 },)*
148 ];
149 };
150}
151
152diagnostic_codes! {
153 TypeMismatch, "HARN-TYP-001", Typ, "expected and actual types are incompatible";
154 InvalidBinaryOperator, "HARN-TYP-002", Typ, "binary operator is not defined for the operand types";
155 StringInterpolationRewrite, "HARN-TYP-003", Typ, "string concatenation should be rewritten as interpolation";
156 ReturnTypeMismatch, "HARN-TYP-004", Typ, "returned expression does not match the declared return type";
157 AssignmentTypeMismatch, "HARN-TYP-005", Typ, "assigned value does not match the target type";
158 ArgumentTypeMismatch, "HARN-TYP-006", Typ, "argument value does not match the parameter type";
159 VariableTypeMismatch, "HARN-TYP-007", Typ, "initializer does not match the declared variable type";
160 ClosureReturnTypeMismatch, "HARN-TYP-008", Typ, "closure return expression does not match its declared type";
161 FieldTypeMismatch, "HARN-TYP-009", Typ, "field value does not match its declared type";
162 MethodTypeMismatch, "HARN-TYP-010", Typ, "method receiver or result type is incompatible";
163 GenericTypeArgumentUnsupported, "HARN-TYP-011", Typ, "callable does not accept type arguments";
164 GenericTypeArgumentMismatch, "HARN-TYP-012", Typ, "type argument does not satisfy the generic parameter";
165 GenericTypeArgumentArity, "HARN-TYP-013", Typ, "generic call has the wrong number of type arguments";
166 TypeParameterArity, "HARN-TYP-014", Typ, "declaration has the wrong number of type parameters";
167 WhereConstraintMismatch, "HARN-TYP-015", Typ, "type argument does not satisfy a where-clause constraint";
168 IterableExpected, "HARN-TYP-016", Typ, "expression must be iterable";
169 InvalidIndexType, "HARN-TYP-017", Typ, "subscript index type is invalid";
170 CallableExpected, "HARN-TYP-018", Typ, "expression must be callable";
171 InvalidCast, "HARN-TYP-019", Typ, "cast cannot be proven valid";
172 UnknownTypeName, "HARN-TYP-020", Typ, "type name cannot be resolved";
173 InvalidVariantUse, "HARN-TYP-021", Typ, "variant type is used in an invalid position";
174 InvalidStructLiteral, "HARN-TYP-022", Typ, "struct literal is invalid";
175 InvalidEnumConstruct, "HARN-TYP-023", Typ, "enum construction is invalid";
176 InvalidPatternBinding, "HARN-TYP-024", Typ, "pattern binding is invalid for the expected type";
177 InvalidOptionalAccess, "HARN-TYP-025", Typ, "optional access is invalid for the receiver type";
178 ParserUnexpectedToken, "HARN-PAR-001", Par, "parser found an unexpected token";
179 ParserUnexpectedEof, "HARN-PAR-002", Par, "parser reached end of file while expecting syntax";
180 ParserUnexpectedCharacter, "HARN-PAR-003", Par, "lexer found an unexpected character";
181 ParserUnterminatedString, "HARN-PAR-004", Par, "string literal is unterminated";
182 ParserUnterminatedBlockComment, "HARN-PAR-005", Par, "block comment is unterminated";
183 UndefinedVariable, "HARN-NAM-001", Nam, "variable name cannot be resolved";
184 UndefinedFunction, "HARN-NAM-002", Nam, "function name cannot be resolved";
185 UnknownAttribute, "HARN-NAM-003", Nam, "attribute name is not recognized";
186 UnknownField, "HARN-NAM-004", Nam, "field name does not exist on the target type";
187 UnknownMethod, "HARN-NAM-005", Nam, "method name does not exist on the receiver type";
188 DuplicateArgument, "HARN-NAM-006", Nam, "argument name is duplicated";
189 UnknownOption, "HARN-NAM-007", Nam, "option key is not recognized";
190 UnknownBuiltin, "HARN-NAM-008", Nam, "builtin name cannot be resolved";
191 DeprecatedFunction, "HARN-NAM-009", Nam, "function call targets a deprecated declaration";
192 UnknownDeclaration, "HARN-NAM-010", Nam, "declaration reference cannot be resolved";
193 InvalidAttributeTarget, "HARN-NAM-011", Nam, "attribute is attached to an unsupported declaration";
194 InvalidAttributeArgument, "HARN-NAM-012", Nam, "attribute argument is invalid";
195 InvalidMainSignature, "HARN-NAM-101", Nam, "`main` entrypoint must take a single `harness: Harness` parameter";
196 CapabilityPayloadInvalid, "HARN-CAP-001", Cap, "capability payload is invalid";
197 HitlMissingApprovalPolicy, "HARN-CAP-002", Cap, "human approval construct is missing policy";
198 HitlInvalidApprovalArgument, "HARN-CAP-003", Cap, "human approval argument is invalid";
199 CapabilityResultUnchecked, "HARN-CAP-004", Cap, "capability result must be checked";
200 CapabilityUnknownOperation, "HARN-CAP-005", Cap, "host capability operation is not declared";
201 CapabilityCallStaticNameRequired, "HARN-CAP-006", Cap, "host capability call must use a static operation name";
202 CapabilityBindingInvalid, "HARN-CAP-007", Cap, "tool host capability binding is invalid";
203 UnknownLlmOption, "HARN-LLM-001", Llm, "LLM option key is not recognized";
204 DeprecatedLlmOption, "HARN-LLM-002", Llm, "LLM option key is deprecated";
205 LlmSchemaMissing, "HARN-LLM-003", Llm, "LLM call is missing schema validation";
206 LlmSchemaInvalid, "HARN-LLM-004", Llm, "LLM schema option is invalid";
207 LlmProviderIdentityBranch, "HARN-LLM-005", Llm, "prompt branches on provider identity instead of capability flags";
208 OrchestrationArity, "HARN-ORC-001", Orc, "orchestration construct has invalid arity";
209 OrchestrationType, "HARN-ORC-002", Orc, "orchestration construct argument has invalid type";
210 AgentDefinitionInvalid, "HARN-ORC-003", Orc, "agent declaration is invalid";
211 WorkflowDefinitionInvalid, "HARN-ORC-004", Orc, "workflow declaration is invalid";
212 ToolDefinitionInvalid, "HARN-ORC-005", Orc, "tool declaration is invalid";
213 PipelineDefinitionInvalid, "HARN-ORC-006", Orc, "pipeline declaration is invalid";
214 InvalidSelectConstruct, "HARN-ORC-007", Orc, "select construct is invalid";
215 UnreachableCode, "HARN-ORC-008", Orc, "statement cannot be reached";
216 FlowInvariantAttributeInvalid, "HARN-ORC-009", Orc, "Flow invariant attribute set is invalid";
217 ExecutionTargetMissing, "HARN-ORC-010", Orc, "execution target path cannot be found";
218 DeprecatedStdlibSymbol, "HARN-STD-001", Std, "stdlib symbol has been renamed or deprecated";
219 StdlibUsageInvalid, "HARN-STD-002", Std, "stdlib call is invalid";
220 BuiltinArity, "HARN-STD-003", Std, "builtin call has invalid arity";
221 PromptTemplateParse, "HARN-PRM-001", Prm, "prompt template cannot be parsed";
222 PromptVariantExplosion, "HARN-PRM-002", Prm, "prompt template has too many capability-aware branches";
223 PromptInjectionRisk, "HARN-PRM-003", Prm, "prompt construction risks direct injection";
224 PromptProviderIdentityBranch, "HARN-PRM-004", Prm, "prompt template branches on provider identity";
225 PromptToolSurfaceUnknown, "HARN-PRM-005", Prm, "prompt references a tool outside the declared surface";
226 PromptToolSurfaceDeferredReference, "HARN-PRM-006", Prm, "prompt references a deferred tool without tool search";
227 PromptTargetMissing, "HARN-PRM-007", Prm, "prompt or template target cannot be found";
228 ModuleImportUnresolved, "HARN-MOD-001", Mod, "module import cannot be resolved";
229 ModuleImportUnused, "HARN-MOD-002", Mod, "module import is unused";
230 ModuleImportOrder, "HARN-MOD-003", Mod, "module imports are not in canonical order";
231 ModuleExportInvalid, "HARN-MOD-004", Mod, "module export is invalid";
232 ModuleImportCollision, "HARN-MOD-005", Mod, "module imports expose colliding names";
233 ModuleReExportConflict, "HARN-MOD-006", Mod, "module re-exports conflict";
234 LintRenamedStdlibSymbol, "HARN-LNT-001", Lnt, "renamed stdlib symbol lint";
235 LintCyclomaticComplexity, "HARN-LNT-002", Lnt, "cyclomatic complexity lint";
236 LintNamingConvention, "HARN-LNT-003", Lnt, "naming convention lint";
237 LintEagerCollectionConversion, "HARN-LNT-004", Lnt, "eager collection conversion lint";
238 LintRedundantClone, "HARN-LNT-005", Lnt, "redundant clone lint";
239 LintLongRunningWithoutCleanup, "HARN-LNT-006", Lnt, "long-running workflow cleanup lint";
240 LintMcpToolAnnotations, "HARN-LNT-007", Lnt, "MCP tool annotations lint";
241 LintPrOpenWithoutSecretScan, "HARN-LNT-008", Lnt, "PR open without secret scan lint";
242 LintShadowVariable, "HARN-LNT-009", Lnt, "shadow variable lint";
243 LintPersonaHookTarget, "HARN-LNT-010", Lnt, "persona hook target lint";
244 LintDeadCodeAfterReturn, "HARN-LNT-011", Lnt, "dead code after return lint";
245 LintLetThenReturn, "HARN-LNT-012", Lnt, "let then return lint";
246 LintUnhandledApprovalResult, "HARN-LNT-013", Lnt, "unhandled approval result lint";
247 LintUnusedVariable, "HARN-LNT-014", Lnt, "unused variable lint";
248 LintUnusedPatternBinding, "HARN-LNT-015", Lnt, "unused pattern binding lint";
249 LintUnusedParameter, "HARN-LNT-016", Lnt, "unused parameter lint";
250 LintUnusedImport, "HARN-LNT-017", Lnt, "unused import lint";
251 LintMutableNeverReassigned, "HARN-LNT-018", Lnt, "mutable never reassigned lint";
252 LintUnusedFunction, "HARN-LNT-019", Lnt, "unused function lint";
253 LintUnusedType, "HARN-LNT-020", Lnt, "unused type lint";
254 LintPersonaBodyMustCallSteps, "HARN-LNT-021", Lnt, "persona body must call steps lint";
255 LintUndefinedFunction, "HARN-LNT-022", Lnt, "undefined function lint";
256 LintPipelineReturnType, "HARN-LNT-023", Lnt, "pipeline return type lint";
257 LintMissingHarndoc, "HARN-LNT-024", Lnt, "missing harndoc lint";
258 LintAssertOutsideTest, "HARN-LNT-025", Lnt, "assert outside test lint";
259 LintPromptInjectionRisk, "HARN-LNT-026", Lnt, "prompt injection risk lint";
260 LintConnectorEffectPolicy, "HARN-LNT-027", Lnt, "connector effect policy lint";
261 LintUnnecessaryCast, "HARN-LNT-028", Lnt, "unnecessary cast lint";
262 LintUntypedDictAccess, "HARN-LNT-029", Lnt, "untyped dict access lint";
263 LintConstantLogicalOperand, "HARN-LNT-030", Lnt, "constant logical operand lint";
264 LintPointlessComparison, "HARN-LNT-031", Lnt, "pointless comparison lint";
265 LintComparisonToBool, "HARN-LNT-032", Lnt, "comparison to bool lint";
266 LintInvalidBinaryOpLiteral, "HARN-LNT-033", Lnt, "invalid binary operator literal lint";
267 LintRedundantNilTernary, "HARN-LNT-034", Lnt, "redundant nil ternary lint";
268 LintEmptyBlock, "HARN-LNT-035", Lnt, "empty block lint";
269 LintUnnecessaryElseReturn, "HARN-LNT-036", Lnt, "unnecessary else return lint";
270 LintDuplicateMatchArm, "HARN-LNT-037", Lnt, "duplicate match arm lint";
271 LintRequireInTest, "HARN-LNT-038", Lnt, "require in test lint";
272 LintBreakOutsideLoop, "HARN-LNT-039", Lnt, "break outside loop lint";
273 LintTemplateParse, "HARN-LNT-040", Lnt, "template parse lint";
274 LintBlankLineBetweenItems, "HARN-LNT-041", Lnt, "blank line between items lint";
275 LintTrailingComma, "HARN-LNT-042", Lnt, "trailing comma lint";
276 LintUnnecessaryParentheses, "HARN-LNT-043", Lnt, "unnecessary parentheses lint";
277 LintTemplateVariantExplosion, "HARN-LNT-044", Lnt, "template variant explosion lint";
278 LintRequireFileHeader, "HARN-LNT-045", Lnt, "require file header lint";
279 LintTemplateProviderIdentityBranch, "HARN-LNT-046", Lnt, "template provider identity branch lint";
280 LintImportOrder, "HARN-LNT-047", Lnt, "import order lint";
281 LintPreferOptionalShorthand, "HARN-LNT-048", Lnt, "prefer optional shorthand lint";
282 LintLegacyDocComment, "HARN-LNT-049", Lnt, "legacy doc comment lint";
283 LintDeprecatedLlmOptions, "HARN-LNT-050", Lnt, "deprecated LLM options lint";
284 LintUnnecessarySafeNavigation, "HARN-LNT-051", Lnt, "unnecessary safe navigation lint";
285 FormatterParseFailed, "HARN-FMT-001", Fmt, "formatter could not parse the source";
286 FormatterWouldReformat, "HARN-FMT-002", Fmt, "source is not in canonical format";
287 FormatterTrailingComma, "HARN-FMT-003", Fmt, "formatter normalized trailing comma layout";
288 ImportResolutionFailed, "HARN-IMP-001", Imp, "import target cannot be resolved";
289 ImportSymbolMissing, "HARN-IMP-002", Imp, "imported symbol does not exist";
290 ImportCycle, "HARN-IMP-003", Imp, "import graph contains a cycle";
291 ImmutableAssignment, "HARN-OWN-001", Own, "immutable binding is reassigned";
292 MutableNeverReassigned, "HARN-OWN-002", Own, "mutable binding is never reassigned";
293 OwnershipEscape, "HARN-OWN-003", Own, "owned value escapes its valid scope";
294 BoundaryValueUnvalidated, "HARN-OWN-004", Own, "unvalidated boundary value is used directly";
295 RescueOutsideFunction, "HARN-RCV-001", Rcv, "rescue construct is outside a function body";
296 TryOutsideFunction, "HARN-RCV-002", Rcv, "try construct is outside a function body";
297 InvalidRescueConstruct, "HARN-RCV-003", Rcv, "rescue construct is invalid";
298 NonExhaustiveMatch, "HARN-MAT-001", Mat, "match expression is not exhaustive";
299 DuplicateMatchArm, "HARN-MAT-002", Mat, "match expression contains a duplicate arm";
300 InvalidMatchPattern, "HARN-MAT-003", Mat, "match pattern is invalid";
301}
302
303impl Code {
304 pub const fn registry() -> &'static [RegistryEntry] {
305 REGISTRY
306 }
307
308 pub const fn related(self) -> &'static [Code] {
313 match self {
314 Code::TypeMismatch => &[
317 Code::AssignmentTypeMismatch,
318 Code::ArgumentTypeMismatch,
319 Code::ReturnTypeMismatch,
320 Code::VariableTypeMismatch,
321 Code::FieldTypeMismatch,
322 ],
323 Code::AssignmentTypeMismatch => &[Code::TypeMismatch, Code::VariableTypeMismatch],
324 Code::ArgumentTypeMismatch => &[Code::TypeMismatch, Code::GenericTypeArgumentMismatch],
325 Code::ReturnTypeMismatch => &[Code::TypeMismatch, Code::ClosureReturnTypeMismatch],
326 Code::VariableTypeMismatch => &[Code::TypeMismatch, Code::AssignmentTypeMismatch],
327 Code::ClosureReturnTypeMismatch => &[Code::ReturnTypeMismatch],
328 Code::FieldTypeMismatch => &[Code::TypeMismatch, Code::InvalidStructLiteral],
329 Code::MethodTypeMismatch => &[Code::TypeMismatch, Code::CallableExpected],
330 Code::GenericTypeArgumentUnsupported => &[
332 Code::GenericTypeArgumentMismatch,
333 Code::GenericTypeArgumentArity,
334 ],
335 Code::GenericTypeArgumentMismatch => &[
336 Code::GenericTypeArgumentArity,
337 Code::WhereConstraintMismatch,
338 ],
339 Code::GenericTypeArgumentArity => {
340 &[Code::GenericTypeArgumentMismatch, Code::TypeParameterArity]
341 }
342 Code::TypeParameterArity => &[Code::GenericTypeArgumentArity],
343 Code::WhereConstraintMismatch => &[Code::GenericTypeArgumentMismatch],
344 Code::UndefinedVariable => &[Code::UndefinedFunction, Code::UnknownDeclaration],
346 Code::UndefinedFunction => &[Code::UnknownBuiltin, Code::UnknownDeclaration],
347 Code::UnknownField => &[Code::UnknownMethod, Code::InvalidStructLiteral],
348 Code::UnknownMethod => &[Code::UnknownField, Code::CallableExpected],
349 Code::UnknownAttribute => {
350 &[Code::InvalidAttributeArgument, Code::InvalidAttributeTarget]
351 }
352 Code::InvalidAttributeArgument => {
353 &[Code::UnknownAttribute, Code::InvalidAttributeTarget]
354 }
355 Code::InvalidAttributeTarget => {
356 &[Code::UnknownAttribute, Code::InvalidAttributeArgument]
357 }
358 Code::LlmSchemaMissing => &[Code::LlmSchemaInvalid, Code::UnknownLlmOption],
360 Code::LlmSchemaInvalid => &[Code::LlmSchemaMissing, Code::UnknownLlmOption],
361 Code::UnknownLlmOption => &[Code::DeprecatedLlmOption, Code::LlmSchemaInvalid],
362 Code::DeprecatedLlmOption => &[Code::UnknownLlmOption],
363 Code::LlmProviderIdentityBranch => &[Code::PromptProviderIdentityBranch],
364 Code::PromptTemplateParse => &[Code::PromptTargetMissing],
366 Code::PromptInjectionRisk => &[Code::LintPromptInjectionRisk],
367 Code::PromptProviderIdentityBranch => &[
368 Code::LlmProviderIdentityBranch,
369 Code::LintTemplateProviderIdentityBranch,
370 ],
371 Code::PromptVariantExplosion => &[Code::LintTemplateVariantExplosion],
372 Code::CapabilityResultUnchecked => {
374 &[Code::RescueOutsideFunction, Code::TryOutsideFunction]
375 }
376 Code::CapabilityUnknownOperation => &[Code::CapabilityCallStaticNameRequired],
377 Code::RescueOutsideFunction => {
379 &[Code::TryOutsideFunction, Code::InvalidRescueConstruct]
380 }
381 Code::TryOutsideFunction => &[Code::RescueOutsideFunction],
382 Code::NonExhaustiveMatch => &[Code::InvalidMatchPattern, Code::DuplicateMatchArm],
383 Code::DuplicateMatchArm => &[Code::NonExhaustiveMatch, Code::LintDuplicateMatchArm],
384 Code::ModuleImportUnresolved => {
386 &[Code::ImportResolutionFailed, Code::ImportSymbolMissing]
387 }
388 Code::ModuleImportUnused => &[Code::LintUnusedImport],
389 Code::ImportResolutionFailed => {
390 &[Code::ModuleImportUnresolved, Code::ImportSymbolMissing]
391 }
392 Code::ImportCycle => &[Code::ImportResolutionFailed],
393 Code::ImmutableAssignment => &[Code::MutableNeverReassigned],
395 Code::MutableNeverReassigned => &[Code::LintMutableNeverReassigned],
396 Code::LintDeprecatedLlmOptions => &[Code::DeprecatedLlmOption, Code::UnknownLlmOption],
398 Code::LintPromptInjectionRisk => &[Code::PromptInjectionRisk],
399 Code::LintTemplateVariantExplosion => &[Code::PromptVariantExplosion],
400 Code::LintTemplateProviderIdentityBranch => &[Code::PromptProviderIdentityBranch],
401 Code::LintRenamedStdlibSymbol => &[Code::DeprecatedStdlibSymbol],
402 Code::LintMutableNeverReassigned => &[Code::MutableNeverReassigned],
403 Code::LintUnusedImport => &[Code::ModuleImportUnused],
404 Code::LintDuplicateMatchArm => &[Code::DuplicateMatchArm],
405 _ => &[],
406 }
407 }
408}
409
410impl fmt::Display for Code {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 f.write_str(self.as_str())
413 }
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub struct ParseCodeError;
419
420impl fmt::Display for ParseCodeError {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 f.write_str("unknown Harn diagnostic code")
423 }
424}
425
426impl std::error::Error for ParseCodeError {}
427
428impl FromStr for Code {
429 type Err = ParseCodeError;
430
431 fn from_str(value: &str) -> Result<Self, Self::Err> {
432 Code::ALL
433 .iter()
434 .copied()
435 .find(|code| code.as_str() == value)
436 .ok_or(ParseCodeError)
437 }
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
451pub enum RepairSafety {
452 FormatOnly,
455 BehaviorPreserving,
458 ScopeLocal,
462 SurfaceChanging,
465 CapabilityChanging,
469 NeedsHuman,
473}
474
475impl RepairSafety {
476 pub const ALL: &'static [RepairSafety] = &[
477 RepairSafety::FormatOnly,
478 RepairSafety::BehaviorPreserving,
479 RepairSafety::ScopeLocal,
480 RepairSafety::SurfaceChanging,
481 RepairSafety::CapabilityChanging,
482 RepairSafety::NeedsHuman,
483 ];
484
485 pub const fn as_str(self) -> &'static str {
489 match self {
490 RepairSafety::FormatOnly => "format-only",
491 RepairSafety::BehaviorPreserving => "behavior-preserving",
492 RepairSafety::ScopeLocal => "scope-local",
493 RepairSafety::SurfaceChanging => "surface-changing",
494 RepairSafety::CapabilityChanging => "capability-changing",
495 RepairSafety::NeedsHuman => "needs-human",
496 }
497 }
498
499 pub const fn is_at_most(self, ceiling: RepairSafety) -> bool {
503 (self as u8) <= (ceiling as u8)
504 }
505}
506
507impl fmt::Display for RepairSafety {
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 f.write_str(self.as_str())
510 }
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub struct ParseRepairSafetyError;
516
517impl fmt::Display for ParseRepairSafetyError {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 f.write_str("unknown Harn repair-safety class")
520 }
521}
522
523impl std::error::Error for ParseRepairSafetyError {}
524
525impl FromStr for RepairSafety {
526 type Err = ParseRepairSafetyError;
527
528 fn from_str(value: &str) -> Result<Self, Self::Err> {
529 RepairSafety::ALL
530 .iter()
531 .copied()
532 .find(|safety| safety.as_str() == value)
533 .ok_or(ParseRepairSafetyError)
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Hash)]
543pub struct RepairId(std::borrow::Cow<'static, str>);
544
545impl RepairId {
546 pub const fn from_static(s: &'static str) -> Self {
547 RepairId(std::borrow::Cow::Borrowed(s))
548 }
549
550 pub fn from_owned(s: String) -> Self {
551 RepairId(std::borrow::Cow::Owned(s))
552 }
553
554 pub fn as_str(&self) -> &str {
555 &self.0
556 }
557}
558
559impl fmt::Display for RepairId {
560 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561 f.write_str(&self.0)
562 }
563}
564
565#[derive(Debug, Clone)]
573pub struct Repair {
574 pub id: RepairId,
575 pub summary: String,
576 pub safety: RepairSafety,
577}
578
579impl Repair {
580 pub fn from_template(template: &RepairTemplate) -> Self {
581 Repair {
582 id: RepairId::from_static(template.id),
583 summary: template.summary.to_string(),
584 safety: template.safety,
585 }
586 }
587}
588
589#[derive(Debug, Clone, Copy)]
596pub struct RepairTemplate {
597 pub id: &'static str,
598 pub summary: &'static str,
599 pub safety: RepairSafety,
600}
601
602impl Code {
603 pub const fn repair_template(self) -> Option<&'static RepairTemplate> {
606 match self {
607 Code::TypeMismatch
609 | Code::ReturnTypeMismatch
610 | Code::AssignmentTypeMismatch
611 | Code::ArgumentTypeMismatch
612 | Code::VariableTypeMismatch
613 | Code::ClosureReturnTypeMismatch
614 | Code::FieldTypeMismatch
615 | Code::MethodTypeMismatch
616 | Code::InvalidIndexType => Some(&REPAIR_INSERT_EXPLICIT_CONVERSION),
617 Code::StringInterpolationRewrite => Some(&REPAIR_REWRITE_STRING_INTERPOLATION),
618 Code::UnknownTypeName => Some(&REPAIR_IMPORTS_FIX_PATH),
619 Code::InvalidCast => Some(&REPAIR_CASTS_REMOVE_UNCHECKED),
620
621 Code::UndefinedVariable
623 | Code::UndefinedFunction
624 | Code::UnknownField
625 | Code::UnknownMethod
626 | Code::UnknownBuiltin
627 | Code::UnknownDeclaration => Some(&REPAIR_BINDINGS_RENAME_TO_CLOSEST),
628 Code::InvalidMainSignature => Some(&REPAIR_BINDINGS_THREAD_HARNESS),
629 Code::DeprecatedFunction => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
630 Code::ModuleImportUnresolved | Code::ImportResolutionFailed => {
631 Some(&REPAIR_IMPORTS_FIX_PATH)
632 }
633 Code::ModuleImportUnused => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
634 Code::ModuleImportOrder => Some(&REPAIR_IMPORTS_REORDER),
635
636 Code::CapabilityResultUnchecked => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
638 Code::CapabilityBindingInvalid => Some(&REPAIR_MANUAL_REVIEW_CAPABILITY),
639 Code::RescueOutsideFunction | Code::TryOutsideFunction => {
640 Some(&REPAIR_ERRORS_WRAP_IN_FN)
641 }
642
643 Code::DeprecatedLlmOption => Some(&REPAIR_LLM_MIGRATE_DEPRECATED_OPTION),
645 Code::LlmSchemaMissing => Some(&REPAIR_LLM_ADD_SCHEMA),
646 Code::LlmProviderIdentityBranch | Code::PromptProviderIdentityBranch => {
647 Some(&REPAIR_LLM_USE_CAPABILITY_FLAG)
648 }
649 Code::PromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
650 Code::PromptToolSurfaceUnknown | Code::PromptToolSurfaceDeferredReference => {
651 Some(&REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE)
652 }
653 Code::PromptVariantExplosion => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
654
655 Code::DeprecatedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
657
658 Code::ImmutableAssignment => Some(&REPAIR_BINDINGS_MAKE_MUTABLE),
660 Code::MutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
661
662 Code::NonExhaustiveMatch => Some(&REPAIR_MATCH_ADD_MISSING_ARMS),
664 Code::DuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
665
666 Code::UnreachableCode => Some(&REPAIR_DEAD_CODE_REMOVE),
668
669 Code::FormatterWouldReformat | Code::FormatterTrailingComma => {
671 Some(&REPAIR_FORMAT_REFORMAT)
672 }
673
674 Code::LintUnusedVariable
676 | Code::LintUnusedPatternBinding
677 | Code::LintUnusedParameter => Some(&REPAIR_BINDINGS_RENAME_UNUSED),
678 Code::LintUnusedImport => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
679 Code::LintUnusedFunction | Code::LintUnusedType => {
680 Some(&REPAIR_DECLARATIONS_REMOVE_UNUSED)
681 }
682 Code::LintMutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
683 Code::LintImportOrder => Some(&REPAIR_IMPORTS_REORDER),
684 Code::LintBlankLineBetweenItems
685 | Code::LintTrailingComma
686 | Code::LintUnnecessaryParentheses
687 | Code::LintRequireFileHeader => Some(&REPAIR_FORMAT_REFORMAT),
688 Code::LintLegacyDocComment => Some(&REPAIR_DOC_COMMENT_MIGRATE),
689 Code::LintEmptyBlock => Some(&REPAIR_BLOCK_REMOVE_EMPTY),
690 Code::LintUnnecessaryElseReturn | Code::LintLetThenReturn => {
691 Some(&REPAIR_CONTROL_FLOW_FLATTEN)
692 }
693 Code::LintRedundantNilTernary
694 | Code::LintUnnecessarySafeNavigation
695 | Code::LintPreferOptionalShorthand
696 | Code::LintComparisonToBool
697 | Code::LintPointlessComparison
698 | Code::LintConstantLogicalOperand => Some(&REPAIR_EXPRESSION_SIMPLIFY),
699 Code::LintUnnecessaryCast => Some(&REPAIR_CASTS_REMOVE_REDUNDANT),
700 Code::LintRedundantClone => Some(&REPAIR_CLONE_REMOVE_REDUNDANT),
701 Code::LintEagerCollectionConversion => Some(&REPAIR_COLLECTION_PREFER_LAZY),
702 Code::LintDeadCodeAfterReturn => Some(&REPAIR_DEAD_CODE_REMOVE),
703 Code::LintRenamedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
704 Code::LintDeprecatedLlmOptions => Some(&REPAIR_LLM_MIGRATE_DEPRECATED_OPTION),
705 Code::LintTemplateProviderIdentityBranch => Some(&REPAIR_LLM_USE_CAPABILITY_FLAG),
706 Code::LintPromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
707 Code::LintShadowVariable => Some(&REPAIR_BINDINGS_RENAME_SHADOW),
708 Code::LintNamingConvention => Some(&REPAIR_STYLE_RENAME_TO_CONVENTION),
709 Code::LintUnhandledApprovalResult => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
710 Code::LintMissingHarndoc => Some(&REPAIR_DOC_ADD_HARNDOC),
711 Code::LintDuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
712 Code::LintUntypedDictAccess => Some(&REPAIR_TYPES_ADD_SHAPE_ANNOTATION),
713 Code::LintMcpToolAnnotations => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
714 Code::LintTemplateVariantExplosion | Code::LintLongRunningWithoutCleanup => {
715 Some(&REPAIR_MANUAL_NEEDS_HUMAN)
716 }
717
718 _ => None,
722 }
723 }
724}
725
726const REPAIR_INSERT_EXPLICIT_CONVERSION: RepairTemplate = RepairTemplate {
741 id: "casts/insert-explicit-conversion",
742 summary: "Insert an explicit conversion or correct the operand type",
743 safety: RepairSafety::ScopeLocal,
744};
745
746const REPAIR_REWRITE_STRING_INTERPOLATION: RepairTemplate = RepairTemplate {
747 id: "style/string-interpolation",
748 summary: "Rewrite string concatenation as an interpolation literal",
749 safety: RepairSafety::BehaviorPreserving,
750};
751
752const REPAIR_CASTS_REMOVE_UNCHECKED: RepairTemplate = RepairTemplate {
753 id: "casts/remove-unchecked",
754 summary: "Remove the unchecked cast or guard it with a type test",
755 safety: RepairSafety::ScopeLocal,
756};
757
758const REPAIR_CASTS_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
759 id: "casts/remove-redundant",
760 summary: "Remove the redundant cast",
761 safety: RepairSafety::BehaviorPreserving,
762};
763
764const REPAIR_BINDINGS_RENAME_TO_CLOSEST: RepairTemplate = RepairTemplate {
765 id: "bindings/rename-to-closest",
766 summary: "Rename to the closest in-scope identifier",
767 safety: RepairSafety::ScopeLocal,
768};
769
770const REPAIR_BINDINGS_MAKE_MUTABLE: RepairTemplate = RepairTemplate {
771 id: "bindings/make-mutable",
772 summary: "Mark the binding `mut` so it can be reassigned",
773 safety: RepairSafety::ScopeLocal,
774};
775
776const REPAIR_BINDINGS_MAKE_IMMUTABLE: RepairTemplate = RepairTemplate {
777 id: "bindings/make-immutable",
778 summary: "Drop `mut` since the binding is never reassigned",
779 safety: RepairSafety::BehaviorPreserving,
780};
781
782const REPAIR_BINDINGS_RENAME_UNUSED: RepairTemplate = RepairTemplate {
783 id: "bindings/rename-unused",
784 summary: "Prefix the unused binding with `_` to silence the lint",
785 safety: RepairSafety::BehaviorPreserving,
786};
787
788const REPAIR_BINDINGS_RENAME_SHADOW: RepairTemplate = RepairTemplate {
789 id: "bindings/rename-shadow",
790 summary: "Rename the shadowing binding to a distinct name",
791 safety: RepairSafety::ScopeLocal,
792};
793
794const REPAIR_BINDINGS_THREAD_HARNESS: RepairTemplate = RepairTemplate {
795 id: "bindings/thread-harness",
796 summary: "Rewrite the entrypoint as `fn main(harness: Harness)` so the runtime can thread its capability handle",
797 safety: RepairSafety::SurfaceChanging,
798};
799
800const REPAIR_DECLARATIONS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
801 id: "declarations/remove-unused",
802 summary: "Remove the unused declaration",
803 safety: RepairSafety::SurfaceChanging,
804};
805
806const REPAIR_IMPORTS_FIX_PATH: RepairTemplate = RepairTemplate {
807 id: "imports/fix-path",
808 summary: "Replace the import path with a resolvable target",
809 safety: RepairSafety::ScopeLocal,
810};
811
812const REPAIR_IMPORTS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
813 id: "imports/remove-unused",
814 summary: "Remove the unused import",
815 safety: RepairSafety::BehaviorPreserving,
816};
817
818const REPAIR_IMPORTS_REORDER: RepairTemplate = RepairTemplate {
819 id: "imports/reorder",
820 summary: "Reorder imports into canonical grouping",
821 safety: RepairSafety::FormatOnly,
822};
823
824const REPAIR_ERRORS_CHECK_OR_RESCUE: RepairTemplate = RepairTemplate {
825 id: "errors/check-or-rescue",
826 summary: "Check the result or wrap the call in a `rescue` block",
827 safety: RepairSafety::ScopeLocal,
828};
829
830const REPAIR_ERRORS_WRAP_IN_FN: RepairTemplate = RepairTemplate {
831 id: "errors/wrap-in-fn",
832 summary: "Move the construct inside a function body",
833 safety: RepairSafety::SurfaceChanging,
834};
835
836const REPAIR_MATCH_ADD_MISSING_ARMS: RepairTemplate = RepairTemplate {
837 id: "match/add-missing-arms",
838 summary: "Add arms covering the missing variants",
839 safety: RepairSafety::ScopeLocal,
840};
841
842const REPAIR_MATCH_REMOVE_DUPLICATE_ARM: RepairTemplate = RepairTemplate {
843 id: "match/remove-duplicate-arm",
844 summary: "Remove the duplicated match arm",
845 safety: RepairSafety::BehaviorPreserving,
846};
847
848const REPAIR_FORMAT_REFORMAT: RepairTemplate = RepairTemplate {
849 id: "format/reformat",
850 summary: "Apply canonical formatting",
851 safety: RepairSafety::FormatOnly,
852};
853
854const REPAIR_DOC_COMMENT_MIGRATE: RepairTemplate = RepairTemplate {
855 id: "doc/migrate-comment-style",
856 summary: "Migrate the legacy comment to canonical doc syntax",
857 safety: RepairSafety::FormatOnly,
858};
859
860const REPAIR_DOC_ADD_HARNDOC: RepairTemplate = RepairTemplate {
861 id: "doc/add-harndoc",
862 summary: "Add a `///` doc comment describing this declaration",
863 safety: RepairSafety::BehaviorPreserving,
864};
865
866const REPAIR_BLOCK_REMOVE_EMPTY: RepairTemplate = RepairTemplate {
867 id: "blocks/remove-empty",
868 summary: "Remove the empty block or fill in an explicit body",
869 safety: RepairSafety::ScopeLocal,
870};
871
872const REPAIR_CONTROL_FLOW_FLATTEN: RepairTemplate = RepairTemplate {
873 id: "control-flow/flatten",
874 summary: "Flatten the unnecessary control flow construct",
875 safety: RepairSafety::BehaviorPreserving,
876};
877
878const REPAIR_EXPRESSION_SIMPLIFY: RepairTemplate = RepairTemplate {
879 id: "expressions/simplify",
880 summary: "Simplify the expression to its canonical form",
881 safety: RepairSafety::BehaviorPreserving,
882};
883
884const REPAIR_CLONE_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
885 id: "clones/remove-redundant",
886 summary: "Remove the redundant clone",
887 safety: RepairSafety::BehaviorPreserving,
888};
889
890const REPAIR_COLLECTION_PREFER_LAZY: RepairTemplate = RepairTemplate {
891 id: "collections/prefer-lazy",
892 summary: "Replace the eager collection step with a lazy variant",
893 safety: RepairSafety::ScopeLocal,
894};
895
896const REPAIR_DEAD_CODE_REMOVE: RepairTemplate = RepairTemplate {
897 id: "control-flow/remove-dead",
898 summary: "Remove the unreachable code",
899 safety: RepairSafety::BehaviorPreserving,
900};
901
902const REPAIR_STDLIB_MIGRATE_RENAMED: RepairTemplate = RepairTemplate {
903 id: "stdlib/migrate-renamed",
904 summary: "Rename the call to the renamed stdlib symbol",
905 safety: RepairSafety::ScopeLocal,
906};
907
908const REPAIR_LLM_MIGRATE_DEPRECATED_OPTION: RepairTemplate = RepairTemplate {
909 id: "llm/migrate-deprecated-option",
910 summary: "Replace the deprecated option with its supported equivalent",
911 safety: RepairSafety::ScopeLocal,
912};
913
914const REPAIR_LLM_ADD_SCHEMA: RepairTemplate = RepairTemplate {
915 id: "llm/add-schema",
916 summary: "Add a typed output schema to the LLM call",
917 safety: RepairSafety::SurfaceChanging,
918};
919
920const REPAIR_LLM_USE_CAPABILITY_FLAG: RepairTemplate = RepairTemplate {
921 id: "llm/use-capability-flag",
922 summary: "Branch on a capability flag instead of provider identity",
923 safety: RepairSafety::CapabilityChanging,
924};
925
926const REPAIR_PROMPTS_ESCAPE_INJECTION: RepairTemplate = RepairTemplate {
927 id: "prompts/escape-injection",
928 summary: "Pass the untrusted input through a structured placeholder",
929 safety: RepairSafety::ScopeLocal,
930};
931
932const REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE: RepairTemplate = RepairTemplate {
933 id: "prompts/add-tool-to-surface",
934 summary: "Add the referenced tool to the declared tool surface",
935 safety: RepairSafety::SurfaceChanging,
936};
937
938const REPAIR_STYLE_RENAME_TO_CONVENTION: RepairTemplate = RepairTemplate {
939 id: "style/rename-to-convention",
940 summary: "Rename to match the casing convention for this kind",
941 safety: RepairSafety::SurfaceChanging,
942};
943
944const REPAIR_TYPES_ADD_SHAPE_ANNOTATION: RepairTemplate = RepairTemplate {
945 id: "types/add-shape-annotation",
946 summary: "Annotate the dict with a concrete shape type",
947 safety: RepairSafety::SurfaceChanging,
948};
949
950const REPAIR_MANUAL_REVIEW_CAPABILITY: RepairTemplate = RepairTemplate {
951 id: "manual/review-capability-binding",
952 summary: "Review the capability binding; the fix is not mechanical",
953 safety: RepairSafety::NeedsHuman,
954};
955
956const REPAIR_MANUAL_NEEDS_HUMAN: RepairTemplate = RepairTemplate {
957 id: "manual/needs-human",
958 summary: "Plan a human-led change; auto-apply is not safe here",
959 safety: RepairSafety::NeedsHuman,
960};
961
962pub const REPAIR_REGISTRY: &[&RepairTemplate] = &[
966 &REPAIR_INSERT_EXPLICIT_CONVERSION,
967 &REPAIR_REWRITE_STRING_INTERPOLATION,
968 &REPAIR_CASTS_REMOVE_UNCHECKED,
969 &REPAIR_CASTS_REMOVE_REDUNDANT,
970 &REPAIR_BINDINGS_RENAME_TO_CLOSEST,
971 &REPAIR_BINDINGS_MAKE_MUTABLE,
972 &REPAIR_BINDINGS_MAKE_IMMUTABLE,
973 &REPAIR_BINDINGS_RENAME_UNUSED,
974 &REPAIR_BINDINGS_RENAME_SHADOW,
975 &REPAIR_BINDINGS_THREAD_HARNESS,
976 &REPAIR_DECLARATIONS_REMOVE_UNUSED,
977 &REPAIR_IMPORTS_FIX_PATH,
978 &REPAIR_IMPORTS_REMOVE_UNUSED,
979 &REPAIR_IMPORTS_REORDER,
980 &REPAIR_ERRORS_CHECK_OR_RESCUE,
981 &REPAIR_ERRORS_WRAP_IN_FN,
982 &REPAIR_MATCH_ADD_MISSING_ARMS,
983 &REPAIR_MATCH_REMOVE_DUPLICATE_ARM,
984 &REPAIR_FORMAT_REFORMAT,
985 &REPAIR_DOC_COMMENT_MIGRATE,
986 &REPAIR_DOC_ADD_HARNDOC,
987 &REPAIR_BLOCK_REMOVE_EMPTY,
988 &REPAIR_CONTROL_FLOW_FLATTEN,
989 &REPAIR_EXPRESSION_SIMPLIFY,
990 &REPAIR_CLONE_REMOVE_REDUNDANT,
991 &REPAIR_COLLECTION_PREFER_LAZY,
992 &REPAIR_DEAD_CODE_REMOVE,
993 &REPAIR_STDLIB_MIGRATE_RENAMED,
994 &REPAIR_LLM_MIGRATE_DEPRECATED_OPTION,
995 &REPAIR_LLM_ADD_SCHEMA,
996 &REPAIR_LLM_USE_CAPABILITY_FLAG,
997 &REPAIR_PROMPTS_ESCAPE_INJECTION,
998 &REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE,
999 &REPAIR_STYLE_RENAME_TO_CONVENTION,
1000 &REPAIR_TYPES_ADD_SHAPE_ANNOTATION,
1001 &REPAIR_MANUAL_REVIEW_CAPABILITY,
1002 &REPAIR_MANUAL_NEEDS_HUMAN,
1003];
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::{Category, Code, ParseRepairSafetyError, RepairSafety, REPAIR_REGISTRY};
1008 use std::collections::HashSet;
1009 use std::str::FromStr;
1010
1011 #[test]
1012 fn parses_registered_code() {
1013 assert_eq!(Code::from_str("HARN-TYP-014"), Ok(Code::TypeParameterArity));
1014 }
1015
1016 #[test]
1017 fn registry_has_unique_identifiers() {
1018 let mut seen = HashSet::new();
1019 for entry in Code::registry() {
1020 assert!(
1021 seen.insert(entry.identifier),
1022 "duplicate diagnostic code {}",
1023 entry.identifier
1024 );
1025 assert_eq!(entry.code.as_str(), entry.identifier);
1026 assert_eq!(entry.code.category(), entry.category);
1027 let expected_prefix = format!("HARN-{}-", entry.category);
1028 assert!(entry.identifier.starts_with(&expected_prefix));
1029 let suffix = entry.identifier.trim_start_matches(&expected_prefix);
1030 assert_eq!(suffix.len(), 3);
1031 assert!(suffix.chars().all(|ch| ch.is_ascii_digit()));
1032 assert!(!entry.summary.is_empty());
1033 }
1034 assert!(Code::registry().len() >= 40);
1035 }
1036
1037 #[test]
1038 fn every_category_is_populated() {
1039 for category in Category::ALL {
1040 assert!(
1041 Code::registry()
1042 .iter()
1043 .any(|entry| entry.category == *category),
1044 "missing diagnostic code category {category}"
1045 );
1046 }
1047 }
1048
1049 #[test]
1050 fn every_code_has_non_empty_explanation() {
1051 for entry in Code::registry() {
1052 let body = entry.code.explanation();
1053 assert!(
1054 !body.trim().is_empty(),
1055 "diagnostic code {} has an empty explanation file",
1056 entry.identifier
1057 );
1058 assert!(
1059 body.contains(entry.identifier),
1060 "explanation for {} should reference its identifier",
1061 entry.identifier
1062 );
1063 }
1064 }
1065
1066 #[test]
1067 fn related_codes_are_registered_and_non_self() {
1068 for entry in Code::registry() {
1069 for &other in entry.code.related() {
1070 assert_ne!(
1071 other, entry.code,
1072 "{} lists itself as a related code",
1073 entry.identifier
1074 );
1075 assert!(
1076 Code::registry().iter().any(|e| e.code == other),
1077 "{} lists unregistered related code {}",
1078 entry.identifier,
1079 other
1080 );
1081 }
1082 }
1083 }
1084
1085 #[test]
1086 fn repair_safety_string_roundtrip() {
1087 for safety in RepairSafety::ALL {
1088 let parsed = RepairSafety::from_str(safety.as_str()).unwrap();
1089 assert_eq!(parsed, *safety);
1090 assert_eq!(parsed.to_string(), safety.as_str());
1091 }
1092 assert_eq!(
1093 RepairSafety::from_str("not-a-safety-class"),
1094 Err(ParseRepairSafetyError)
1095 );
1096 }
1097
1098 #[test]
1099 fn repair_safety_ordering_is_monotonic_low_to_high() {
1100 let order = RepairSafety::ALL;
1104 for window in order.windows(2) {
1105 assert!(
1106 window[0] < window[1],
1107 "{:?} should be safer than {:?}",
1108 window[0],
1109 window[1]
1110 );
1111 assert!(window[0].is_at_most(window[1]));
1112 assert!(!window[1].is_at_most(window[0]));
1113 }
1114 }
1115
1116 #[test]
1117 fn repair_registry_has_at_least_twenty_entries() {
1118 assert!(
1119 REPAIR_REGISTRY.len() >= 20,
1120 "expected ≥20 repair templates, found {}",
1121 REPAIR_REGISTRY.len()
1122 );
1123 }
1124
1125 #[test]
1126 fn repair_ids_are_kebab_case_namespaced_and_unique() {
1127 let mut seen = HashSet::new();
1128 for template in REPAIR_REGISTRY {
1129 assert!(
1130 seen.insert(template.id),
1131 "duplicate repair id {}",
1132 template.id
1133 );
1134 let (namespace, leaf) = template.id.split_once('/').unwrap_or_else(|| {
1135 panic!(
1136 "repair id `{}` is missing `<namespace>/` prefix",
1137 template.id
1138 )
1139 });
1140 assert!(
1141 !namespace.is_empty() && !leaf.is_empty(),
1142 "repair id `{}` has empty namespace or leaf",
1143 template.id
1144 );
1145 for ch in template.id.chars() {
1146 assert!(
1147 ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '/',
1148 "repair id `{}` has non-kebab character {ch:?}",
1149 template.id
1150 );
1151 }
1152 assert!(
1153 !template.summary.is_empty(),
1154 "repair {} has empty summary",
1155 template.id
1156 );
1157 let first = template.summary.chars().next().unwrap();
1159 assert!(
1160 first.is_ascii_uppercase(),
1161 "repair {} summary `{}` should start with a capital",
1162 template.id,
1163 template.summary
1164 );
1165 }
1166 }
1167
1168 #[test]
1169 fn manual_namespace_is_needs_human() {
1170 for template in REPAIR_REGISTRY {
1171 if let Some(("manual", _)) = template.id.split_once('/') {
1172 assert_eq!(
1173 template.safety,
1174 RepairSafety::NeedsHuman,
1175 "manual/* repair {} must be NeedsHuman",
1176 template.id
1177 );
1178 }
1179 }
1180 }
1181
1182 #[test]
1183 fn known_codes_carry_expected_safety_class() {
1184 let expected: &[(Code, RepairSafety, &str)] = &[
1188 (
1189 Code::FormatterWouldReformat,
1190 RepairSafety::FormatOnly,
1191 "format/reformat",
1192 ),
1193 (
1194 Code::ModuleImportUnused,
1195 RepairSafety::BehaviorPreserving,
1196 "imports/remove-unused",
1197 ),
1198 (
1199 Code::ImmutableAssignment,
1200 RepairSafety::ScopeLocal,
1201 "bindings/make-mutable",
1202 ),
1203 (
1204 Code::LintUnusedFunction,
1205 RepairSafety::SurfaceChanging,
1206 "declarations/remove-unused",
1207 ),
1208 (
1209 Code::LlmProviderIdentityBranch,
1210 RepairSafety::CapabilityChanging,
1211 "llm/use-capability-flag",
1212 ),
1213 (
1214 Code::PromptVariantExplosion,
1215 RepairSafety::NeedsHuman,
1216 "manual/needs-human",
1217 ),
1218 (
1219 Code::NonExhaustiveMatch,
1220 RepairSafety::ScopeLocal,
1221 "match/add-missing-arms",
1222 ),
1223 ];
1224 for (code, safety, repair_id) in expected {
1225 let template = code
1226 .repair_template()
1227 .unwrap_or_else(|| panic!("{code} should have a repair template"));
1228 assert_eq!(template.safety, *safety, "{code} safety class drifted");
1229 assert_eq!(template.id, *repair_id, "{code} repair id drifted");
1230 }
1231 }
1232
1233 #[test]
1234 fn repair_templates_cover_at_least_twenty_codes() {
1235 let covered = Code::ALL
1236 .iter()
1237 .filter(|code| code.repair_template().is_some())
1238 .count();
1239 assert!(
1240 covered >= 20,
1241 "expected ≥20 codes with a repair template, found {covered}"
1242 );
1243 }
1244
1245 #[test]
1246 fn every_registered_repair_is_referenced_by_some_code() {
1247 let referenced: HashSet<&'static str> = Code::ALL
1248 .iter()
1249 .filter_map(|code| code.repair_template())
1250 .map(|template| template.id)
1251 .collect();
1252 for template in REPAIR_REGISTRY {
1253 assert!(
1254 referenced.contains(template.id),
1255 "repair {} is in REPAIR_REGISTRY but no Code maps to it",
1256 template.id
1257 );
1258 }
1259 }
1260}