1use std::path::PathBuf;
2
3use serde::{Serialize, Serializer, ser::SerializeStruct};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum LintProfile {
8 Core,
9 Surface,
10 #[default]
11 Strict,
12}
13
14impl LintProfile {
15 pub fn as_str(self) -> &'static str {
16 match self {
17 Self::Core => "core",
18 Self::Surface => "surface",
19 Self::Strict => "strict",
20 }
21 }
22}
23
24impl std::str::FromStr for LintProfile {
25 type Err = String;
26
27 fn from_str(raw: &str) -> Result<Self, Self::Err> {
28 match raw {
29 "core" => Ok(Self::Core),
30 "surface" => Ok(Self::Surface),
31 "strict" => Ok(Self::Strict),
32 _ => Err(format!(
33 "invalid profile `{raw}`; expected core|surface|strict"
34 )),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct DiagnosticCodeInfo {
41 pub profile: LintProfile,
42 pub summary: &'static str,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
46pub enum DiagnosticLevel {
47 Warning,
48 Error,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
52pub enum DiagnosticClass {
53 ToolError,
54 ToolWarning,
55 PolicyError { code: String },
56 PolicyWarning { code: String },
57 AdvisoryWarning { code: String },
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
61#[serde(rename_all = "snake_case")]
62pub enum DiagnosticFixKind {
63 ReplacePath,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
67pub struct DiagnosticFix {
68 pub kind: DiagnosticFixKind,
69 pub replacement: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
73pub struct Diagnostic {
74 pub class: DiagnosticClass,
75 pub file: Option<PathBuf>,
76 pub line: Option<usize>,
77 pub fix: Option<DiagnosticFix>,
78 pub message: String,
79}
80
81impl Diagnostic {
82 pub fn error(file: Option<PathBuf>, line: Option<usize>, message: impl Into<String>) -> Self {
83 Self {
84 class: DiagnosticClass::ToolError,
85 file,
86 line,
87 fix: None,
88 message: message.into(),
89 }
90 }
91
92 pub fn warning(file: Option<PathBuf>, line: Option<usize>, message: impl Into<String>) -> Self {
93 Self {
94 class: DiagnosticClass::ToolWarning,
95 file,
96 line,
97 fix: None,
98 message: message.into(),
99 }
100 }
101
102 pub fn policy(
103 file: Option<PathBuf>,
104 line: Option<usize>,
105 code: impl Into<String>,
106 message: impl Into<String>,
107 ) -> Self {
108 Self {
109 class: DiagnosticClass::PolicyWarning { code: code.into() },
110 file,
111 line,
112 fix: None,
113 message: message.into(),
114 }
115 }
116
117 pub fn policy_error(
118 file: Option<PathBuf>,
119 line: Option<usize>,
120 code: impl Into<String>,
121 message: impl Into<String>,
122 ) -> Self {
123 Self {
124 class: DiagnosticClass::PolicyError { code: code.into() },
125 file,
126 line,
127 fix: None,
128 message: message.into(),
129 }
130 }
131
132 pub fn advisory(
133 file: Option<PathBuf>,
134 line: Option<usize>,
135 code: impl Into<String>,
136 message: impl Into<String>,
137 ) -> Self {
138 Self {
139 class: DiagnosticClass::AdvisoryWarning { code: code.into() },
140 file,
141 line,
142 fix: None,
143 message: message.into(),
144 }
145 }
146
147 pub fn with_fix(mut self, fix: DiagnosticFix) -> Self {
148 self.fix = Some(fix);
149 self
150 }
151
152 pub fn level(&self) -> DiagnosticLevel {
153 match self.class {
154 DiagnosticClass::ToolError | DiagnosticClass::PolicyError { .. } => {
155 DiagnosticLevel::Error
156 }
157 DiagnosticClass::ToolWarning
158 | DiagnosticClass::PolicyWarning { .. }
159 | DiagnosticClass::AdvisoryWarning { .. } => DiagnosticLevel::Warning,
160 }
161 }
162
163 pub fn code(&self) -> Option<&str> {
164 match &self.class {
165 DiagnosticClass::PolicyError { code }
166 | DiagnosticClass::PolicyWarning { code }
167 | DiagnosticClass::AdvisoryWarning { code } => Some(code),
168 DiagnosticClass::ToolError | DiagnosticClass::ToolWarning => None,
169 }
170 }
171
172 pub fn profile(&self) -> Option<LintProfile> {
173 self.code()
174 .and_then(|code| diagnostic_code_info(code).map(|info| info.profile))
175 }
176
177 pub fn is_error(&self) -> bool {
178 matches!(
179 self.class,
180 DiagnosticClass::ToolError | DiagnosticClass::PolicyError { .. }
181 )
182 }
183
184 pub fn is_policy_warning(&self) -> bool {
185 matches!(self.class, DiagnosticClass::PolicyWarning { .. })
186 }
187
188 pub fn is_advisory_warning(&self) -> bool {
189 matches!(
190 self.class,
191 DiagnosticClass::ToolWarning | DiagnosticClass::AdvisoryWarning { .. }
192 )
193 }
194
195 pub fn is_policy_violation(&self) -> bool {
196 matches!(
197 self.class,
198 DiagnosticClass::PolicyError { .. } | DiagnosticClass::PolicyWarning { .. }
199 )
200 }
201
202 pub fn included_in_profile(&self, profile: LintProfile) -> bool {
203 match &self.class {
204 DiagnosticClass::ToolError | DiagnosticClass::ToolWarning => true,
205 DiagnosticClass::PolicyError { code }
206 | DiagnosticClass::PolicyWarning { code }
207 | DiagnosticClass::AdvisoryWarning { code } => {
208 profile >= minimum_profile_for_code(code)
209 }
210 }
211 }
212}
213
214impl Serialize for Diagnostic {
215 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
216 where
217 S: Serializer,
218 {
219 let mut state = serializer.serialize_struct("Diagnostic", 8)?;
220 state.serialize_field("level", &self.level())?;
221 state.serialize_field("file", &self.file)?;
222 state.serialize_field("line", &self.line)?;
223 state.serialize_field("code", &self.code())?;
224 state.serialize_field("profile", &self.profile())?;
225 state.serialize_field("policy", &self.is_policy_violation())?;
226 state.serialize_field("fix", &self.fix)?;
227 state.serialize_field("message", &self.message)?;
228 state.end()
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum DiagnosticSelection {
234 All,
235 Policy,
236 Advisory,
237}
238
239impl DiagnosticSelection {
240 pub fn includes(self, diagnostic: &Diagnostic) -> bool {
241 match self {
242 Self::All => true,
243 Self::Policy => diagnostic.is_error() || diagnostic.is_policy_violation(),
244 Self::Advisory => diagnostic.is_error() || diagnostic.is_advisory_warning(),
245 }
246 }
247
248 pub fn report_label(self) -> Option<&'static str> {
249 match self {
250 Self::All => None,
251 Self::Policy => Some("policy diagnostics and errors only"),
252 Self::Advisory => Some("advisory diagnostics and errors only"),
253 }
254 }
255}
256
257pub fn diagnostic_code_info(code: &str) -> Option<DiagnosticCodeInfo> {
258 let (profile, summary) = match code {
259 "namespace_flat_use" => (
260 LintProfile::Core,
261 "Flattened imports hide useful namespace context for generic leaves.",
262 ),
263 "namespace_flat_use_preserve_module" => (
264 LintProfile::Core,
265 "Flattened imports hide a module that should stay visible at call sites.",
266 ),
267 "namespace_flat_use_redundant_leaf_context" => (
268 LintProfile::Core,
269 "Flattened imports keep parent context in the leaf instead of the path.",
270 ),
271 "namespace_redundant_qualified_generic" => (
272 LintProfile::Core,
273 "Qualified paths repeat a generic category that the leaf already names.",
274 ),
275 "namespace_aliased_qualified_path" => (
276 LintProfile::Core,
277 "A namespace alias flattens a semantic path instead of keeping the real module visible.",
278 ),
279 "namespace_parent_surface" => (
280 LintProfile::Core,
281 "Imports bypass a canonical parent surface that already re-exports the binding.",
282 ),
283 "namespace_flat_type_alias" => (
284 LintProfile::Core,
285 "A type alias hides useful namespace context for a generic aliased leaf.",
286 ),
287 "namespace_flat_type_alias_preserve_module" => (
288 LintProfile::Core,
289 "A type alias hides a module that should stay visible in the aliased type path.",
290 ),
291 "namespace_flat_type_alias_redundant_leaf_context" => (
292 LintProfile::Core,
293 "A type alias keeps redundant parent context in the alias name instead of the path.",
294 ),
295 "namespace_prelude_glob_import" => (
296 LintProfile::Core,
297 "A prelude glob import hides the real source modules instead of keeping useful namespace context visible.",
298 ),
299 "namespace_glob_preserve_module" => (
300 LintProfile::Core,
301 "A glob import flattens a configured namespace-preserving module instead of keeping that module visible.",
302 ),
303 "internal_catch_all_module" => (
304 LintProfile::Core,
305 "An internal module name is a catch-all bucket instead of a stable domain or facet.",
306 ),
307 "internal_repeated_module_segment" => (
308 LintProfile::Core,
309 "An internal nested module repeats the same segment instead of adding meaning.",
310 ),
311 "internal_organizational_submodule_flatten" => (
312 LintProfile::Core,
313 "An internal organizational module leaks category structure that should usually be flattened.",
314 ),
315 "internal_weak_module_generic_leaf" => (
316 LintProfile::Core,
317 "An internal item leaf is too generic for a weak or technical parent module.",
318 ),
319 "internal_redundant_leaf_context" => (
320 LintProfile::Core,
321 "An internal item leaf repeats context the parent module already provides.",
322 ),
323 "internal_adapter_redundant_leaf_context" => (
324 LintProfile::Core,
325 "An internal adapter leaf repeats implementation context the parent module already provides.",
326 ),
327 "internal_redundant_category_suffix" => (
328 LintProfile::Core,
329 "An internal item leaf repeats the parent category in a redundant suffix.",
330 ),
331 "api_catch_all_module" => (
332 LintProfile::Core,
333 "A surface-visible module is a catch-all bucket instead of a stable domain or facet.",
334 ),
335 "api_repeated_module_segment" => (
336 LintProfile::Core,
337 "A surface-visible nested module repeats the same segment instead of adding meaning.",
338 ),
339 "namespace_flat_pub_use" => (
340 LintProfile::Surface,
341 "A re-export flattens useful namespace context out of the caller-facing path.",
342 ),
343 "namespace_flat_pub_use_preserve_module" => (
344 LintProfile::Surface,
345 "A re-export hides a module that should stay visible in the caller-facing path.",
346 ),
347 "namespace_flat_pub_use_redundant_leaf_context" => (
348 LintProfile::Surface,
349 "A re-export keeps parent context in the leaf instead of the path.",
350 ),
351 "api_missing_parent_surface_export" => (
352 LintProfile::Surface,
353 "A child module surface should usually also expose a readable parent binding.",
354 ),
355 "api_anyhow_error_surface" => (
356 LintProfile::Surface,
357 "A caller-facing surface leaks `anyhow` instead of exposing a crate-owned typed error boundary.",
358 ),
359 "api_semantic_string_scalar" => (
360 LintProfile::Surface,
361 "A caller-facing semantic scalar is kept as a raw string instead of a typed boundary value.",
362 ),
363 "api_semantic_numeric_scalar" => (
364 LintProfile::Surface,
365 "A caller-facing semantic scalar is kept as a raw integer instead of a typed boundary value.",
366 ),
367 "api_weak_module_generic_leaf" => (
368 LintProfile::Surface,
369 "A surface-visible item leaf is too generic for a weak or technical parent module.",
370 ),
371 "api_redundant_leaf_context" => (
372 LintProfile::Surface,
373 "A surface-visible item leaf repeats context the parent module already provides.",
374 ),
375 "api_redundant_category_suffix" => (
376 LintProfile::Surface,
377 "A surface-visible item leaf repeats the parent category in a redundant suffix.",
378 ),
379 "api_organizational_submodule_flatten" => (
380 LintProfile::Surface,
381 "A surface-visible organizational module should usually be flattened out of the path.",
382 ),
383 "api_candidate_semantic_module" => (
384 LintProfile::Strict,
385 "A family of sibling items suggests a stronger semantic module surface.",
386 ),
387 "api_candidate_semantic_module_unsupported_construct" => (
388 LintProfile::Strict,
389 "Semantic-module family inference was skipped because the parsed source contains unsupported constructs.",
390 ),
391 "api_manual_enum_string_helper" => (
392 LintProfile::Strict,
393 "A public enum exposes manual string helpers that should usually be standard traits or derives.",
394 ),
395 "api_ad_hoc_parse_helper" => (
396 LintProfile::Strict,
397 "A public enum parsing helper should usually be modeled as `FromStr` or `TryFrom<&str>`.",
398 ),
399 "api_parallel_enum_metadata_helper" => (
400 LintProfile::Strict,
401 "Parallel enum metadata helpers suggest a typed descriptor surface instead of repeated matches.",
402 ),
403 "api_strum_serialize_all_candidate" => (
404 LintProfile::Strict,
405 "Per-variant `strum` strings could be replaced by one enum-level `serialize_all` rule.",
406 ),
407 "api_builder_candidate" => (
408 LintProfile::Strict,
409 "A configuration-heavy entrypoint would read better as a builder or typed options surface.",
410 ),
411 "api_repeated_parameter_cluster" => (
412 LintProfile::Strict,
413 "Several entrypoints repeat the same positional parameter cluster instead of sharing a typed shape.",
414 ),
415 "api_optional_parameter_builder" => (
416 LintProfile::Strict,
417 "Optional positional parameters suggest a builder so callers can omit unset values.",
418 ),
419 "api_defaulted_optional_parameter" => (
420 LintProfile::Strict,
421 "Defaulted optional positional parameters suggest a builder rather than `None`-passing.",
422 ),
423 "callsite_maybe_some" => (
424 LintProfile::Strict,
425 "A `maybe_*` call wraps a direct value in `Some(...)` instead of using the direct setter or forwarding an existing option.",
426 ),
427 "api_standalone_builder_surface" => (
428 LintProfile::Strict,
429 "Parallel `with_*` or `set_*` free functions suggest a real builder surface.",
430 ),
431 "api_boolean_protocol_decision" => (
432 LintProfile::Strict,
433 "A boolean encodes a domain or protocol decision that should usually be typed.",
434 ),
435 "api_boolean_flag_cluster" => (
436 LintProfile::Strict,
437 "Several booleans jointly shape behavior and suggest a typed mode or options surface.",
438 ),
439 "api_forwarding_compat_wrapper" => (
440 LintProfile::Strict,
441 "A helper only forwards to an existing standard conversion trait.",
442 ),
443 "api_string_error_surface" => (
444 LintProfile::Strict,
445 "A caller-facing error surface is carried as raw strings instead of a typed error boundary.",
446 ),
447 "api_manual_error_surface" => (
448 LintProfile::Strict,
449 "A public error manually exposes formatting and error boilerplate instead of a smaller typed boundary.",
450 ),
451 "api_raw_key_value_bag" => (
452 LintProfile::Strict,
453 "A caller-facing metadata or bag surface is modeled as raw string key-value pairs instead of a typed shape.",
454 ),
455 "api_stringly_protocol_collection" => (
456 LintProfile::Strict,
457 "Protocol or state collections are modeled as raw strings instead of typed values.",
458 ),
459 "api_stringly_protocol_parameter" => (
460 LintProfile::Strict,
461 "A boundary takes protocol or state descriptors as raw strings instead of typed values.",
462 ),
463 "api_stringly_model_scaffold" => (
464 LintProfile::Strict,
465 "A model carries semantic descriptor fields as raw strings instead of typed structure.",
466 ),
467 "api_integer_protocol_parameter" => (
468 LintProfile::Strict,
469 "A caller-facing protocol concept is modeled as a raw integer instead of a typed enum or newtype.",
470 ),
471 "api_raw_id_surface" => (
472 LintProfile::Strict,
473 "A caller-facing id is modeled as a raw string or primitive integer instead of a typed id value.",
474 ),
475 "api_manual_flag_set" => (
476 LintProfile::Strict,
477 "Parallel integer flag constants suggest a typed flags boundary instead of manual bit masks.",
478 ),
479 _ => return None,
480 };
481
482 Some(DiagnosticCodeInfo { profile, summary })
483}
484
485fn minimum_profile_for_code(code: &str) -> LintProfile {
486 diagnostic_code_info(code)
487 .map(|info| info.profile)
488 .unwrap_or(LintProfile::Strict)
489}
490
491impl std::str::FromStr for DiagnosticSelection {
492 type Err = String;
493
494 fn from_str(raw: &str) -> Result<Self, Self::Err> {
495 match raw {
496 "all" => Ok(Self::All),
497 "policy" => Ok(Self::Policy),
498 "advisory" => Ok(Self::Advisory),
499 _ => Err(format!(
500 "invalid show mode `{raw}`; expected all|policy|advisory"
501 )),
502 }
503 }
504}