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