1use serde::{Deserialize, Serialize};
2use std::{fmt, path::PathBuf};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
6pub enum Safety {
7 Safe,
8 Review,
9 Unsafe,
10 Unsupported,
11 NoOp,
12}
13
14impl Safety {
15 pub const fn label(self) -> &'static str {
16 match self {
17 Self::Safe => "SAFE",
18 Self::Review => "REVIEW",
19 Self::Unsafe => "UNSAFE",
20 Self::Unsupported => "UNSUPPORTED",
21 Self::NoOp => "NO_OP",
22 }
23 }
24}
25
26impl fmt::Display for Safety {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.write_str(self.label())
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "kebab-case")]
34pub enum SafetyLevel {
35 AnalysisOnly,
36 FormattingOnly,
37 ProvenLocalRefactor,
38 SemanticReview,
39 Architectural,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "kebab-case")]
44pub enum RuleId {
45 NestPseudoClass,
46 NestPseudoElement,
47 NestAttribute,
48 NestCompound,
49 NestDescendant,
50 NestCombinator,
51 NestMedia,
52 NestSupports,
53 NestContainer,
54 NestStartingStyle,
55 FactorSelectorList,
56 ConsolidateNot,
57 ModernizeIs,
58 ModernizeWhere,
59 ModernizeMediaRange,
60 MergeSameNamedLayer,
61 MergeAdjacentMedia,
62 MergeAdjacentSupports,
63 MergeAdjacentContainer,
64 MergeIdenticalScope,
65 MergeIdenticalStartingStyle,
66 MergeAdjacentIdenticalSelector,
67 MergeIdenticalRuleBodies,
68 FactorIdenticalStatesWithIs,
69 GatherRelatedSelectorRules,
70 PruneOverriddenDeclarations,
71}
72
73impl RuleId {
74 pub const ALL: [RuleId; 26] = [
75 RuleId::NestPseudoClass,
76 RuleId::NestPseudoElement,
77 RuleId::NestAttribute,
78 RuleId::NestCompound,
79 RuleId::NestDescendant,
80 RuleId::NestCombinator,
81 RuleId::NestMedia,
82 RuleId::NestSupports,
83 RuleId::NestContainer,
84 RuleId::NestStartingStyle,
85 RuleId::FactorSelectorList,
86 RuleId::ConsolidateNot,
87 RuleId::ModernizeIs,
88 RuleId::ModernizeWhere,
89 RuleId::ModernizeMediaRange,
90 RuleId::MergeSameNamedLayer,
91 RuleId::MergeAdjacentMedia,
92 RuleId::MergeAdjacentSupports,
93 RuleId::MergeAdjacentContainer,
94 RuleId::MergeIdenticalScope,
95 RuleId::MergeIdenticalStartingStyle,
96 RuleId::MergeAdjacentIdenticalSelector,
97 RuleId::MergeIdenticalRuleBodies,
98 RuleId::FactorIdenticalStatesWithIs,
99 RuleId::GatherRelatedSelectorRules,
100 RuleId::PruneOverriddenDeclarations,
101 ];
102
103 pub const fn as_str(self) -> &'static str {
104 match self {
105 Self::NestPseudoClass => "nest-pseudo-class",
106 Self::NestPseudoElement => "nest-pseudo-element",
107 Self::NestAttribute => "nest-attribute",
108 Self::NestCompound => "nest-compound",
109 Self::NestDescendant => "nest-descendant",
110 Self::NestCombinator => "nest-combinator",
111 Self::NestMedia => "nest-media",
112 Self::NestSupports => "nest-supports",
113 Self::NestContainer => "nest-container",
114 Self::NestStartingStyle => "nest-starting-style",
115 Self::FactorSelectorList => "factor-selector-list",
116 Self::ConsolidateNot => "consolidate-not",
117 Self::ModernizeIs => "modernize-is",
118 Self::ModernizeWhere => "modernize-where",
119 Self::ModernizeMediaRange => "modernize-media-range-syntax",
120 Self::MergeSameNamedLayer => "merge-same-named-layer",
121 Self::MergeAdjacentMedia => "merge-adjacent-media",
122 Self::MergeAdjacentSupports => "merge-adjacent-supports",
123 Self::MergeAdjacentContainer => "merge-adjacent-container",
124 Self::MergeIdenticalScope => "merge-identical-scope",
125 Self::MergeIdenticalStartingStyle => "merge-identical-starting-style",
126 Self::MergeAdjacentIdenticalSelector => "merge-adjacent-identical-selector",
127 Self::MergeIdenticalRuleBodies => "merge-identical-rule-bodies",
128 Self::FactorIdenticalStatesWithIs => "factor-identical-states-with-is",
129 Self::GatherRelatedSelectorRules => "gather-related-selector-rules",
130 Self::PruneOverriddenDeclarations => "prune-overridden-declarations",
131 }
132 }
133}
134
135impl fmt::Display for RuleId {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.write_str(self.as_str())
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
142#[serde(rename_all = "kebab-case")]
143pub enum RuleSection {
144 Modernize,
145 Refactor,
146}
147
148impl RuleSection {
149 pub const ALL: [RuleSection; 2] = [RuleSection::Modernize, RuleSection::Refactor];
150
151 pub const fn label(self) -> &'static str {
152 match self {
153 Self::Modernize => "MODERNIZE (Native Nesting, Range Syntax & Selectors)",
154 Self::Refactor => "REFACTOR (Consolidation, Deduplication & Structural Cleanup)",
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
160pub struct RuleDefinition {
161 pub id: RuleId,
162 pub section: RuleSection,
163 pub title: &'static str,
164 pub category: &'static str,
165 pub safety_level: SafetyLevel,
166 pub description: &'static str,
167}
168
169pub fn rule_definitions() -> Vec<RuleDefinition> {
170 vec![
171 RuleDefinition {
172 id: RuleId::NestPseudoClass,
173 section: RuleSection::Modernize,
174 title: "Nest pseudo-classes",
175 category: "Nesting",
176 safety_level: SafetyLevel::ProvenLocalRefactor,
177 description: "Nest adjacent same-parent pseudo-class rules such as .button:hover -> &:hover.",
178 },
179 RuleDefinition {
180 id: RuleId::NestPseudoElement,
181 section: RuleSection::Modernize,
182 title: "Nest pseudo-elements",
183 category: "Nesting",
184 safety_level: SafetyLevel::ProvenLocalRefactor,
185 description: "Nest adjacent same-parent pseudo-elements such as .card::before -> &::before.",
186 },
187 RuleDefinition {
188 id: RuleId::NestAttribute,
189 section: RuleSection::Modernize,
190 title: "Nest attribute states",
191 category: "Nesting",
192 safety_level: SafetyLevel::ProvenLocalRefactor,
193 description: "Nest adjacent attribute states such as .button[disabled] -> &[disabled].",
194 },
195 RuleDefinition {
196 id: RuleId::NestCompound,
197 section: RuleSection::Modernize,
198 title: "Nest compound states",
199 category: "Nesting",
200 safety_level: SafetyLevel::ProvenLocalRefactor,
201 description: "Nest adjacent compound states such as .item.active -> &.active.",
202 },
203 RuleDefinition {
204 id: RuleId::NestDescendant,
205 section: RuleSection::Modernize,
206 title: "Nest descendants",
207 category: "Nesting",
208 safety_level: SafetyLevel::ProvenLocalRefactor,
209 description: "Nest adjacent descendant selectors (.card .title -> .title) with exact relationship proof.",
210 },
211 RuleDefinition {
212 id: RuleId::NestCombinator,
213 section: RuleSection::Modernize,
214 title: "Nest combinators",
215 category: "Nesting",
216 safety_level: SafetyLevel::ProvenLocalRefactor,
217 description: "Nest adjacent child and sibling combinators (> + ~) under their exact parent selector.",
218 },
219 RuleDefinition {
220 id: RuleId::NestMedia,
221 section: RuleSection::Modernize,
222 title: "Nest local @media",
223 category: "At-rules",
224 safety_level: SafetyLevel::ProvenLocalRefactor,
225 description: "Nest an immediately-following @media block containing matching selector rules.",
226 },
227 RuleDefinition {
228 id: RuleId::NestSupports,
229 section: RuleSection::Modernize,
230 title: "Nest local @supports",
231 category: "At-rules",
232 safety_level: SafetyLevel::ProvenLocalRefactor,
233 description: "Nest an immediately-following @supports block containing matching selector rules.",
234 },
235 RuleDefinition {
236 id: RuleId::NestContainer,
237 section: RuleSection::Modernize,
238 title: "Nest local @container",
239 category: "At-rules",
240 safety_level: SafetyLevel::SemanticReview,
241 description: "Nest an immediately-following @container block for matching selector rules.",
242 },
243 RuleDefinition {
244 id: RuleId::NestStartingStyle,
245 section: RuleSection::Modernize,
246 title: "Nest @starting-style",
247 category: "At-rules",
248 safety_level: SafetyLevel::ProvenLocalRefactor,
249 description: "Nest an immediately-following @starting-style block for the matching parent selector.",
250 },
251 RuleDefinition {
252 id: RuleId::FactorSelectorList,
253 section: RuleSection::Modernize,
254 title: "Factor selector lists",
255 category: "Nesting",
256 safety_level: SafetyLevel::ProvenLocalRefactor,
257 description: "Factor comma-separated selectors sharing a common base (.marker, .marker::before -> .marker { &, &::before }).",
258 },
259 RuleDefinition {
260 id: RuleId::ConsolidateNot,
261 section: RuleSection::Modernize,
262 title: "Consolidate :not() selectors",
263 category: "Selectors",
264 safety_level: SafetyLevel::SemanticReview,
265 description: "Consolidate chained :not() selectors like :not(a):not(b) into :not(a, b) (review required for additive specificity change).",
266 },
267 RuleDefinition {
268 id: RuleId::ModernizeIs,
269 section: RuleSection::Modernize,
270 title: "Factor with :is()",
271 category: "Selectors",
272 safety_level: SafetyLevel::ProvenLocalRefactor,
273 description: "Factor selector-list alternatives with uniform specificity into :is(...) grouping.",
274 },
275 RuleDefinition {
276 id: RuleId::ModernizeWhere,
277 section: RuleSection::Modernize,
278 title: "Modernize with :where()",
279 category: "Selectors",
280 safety_level: SafetyLevel::Architectural,
281 description: "Factor selector-list alternatives into :where(...) for zero-specificity defaults (review required).",
282 },
283 RuleDefinition {
284 id: RuleId::ModernizeMediaRange,
285 section: RuleSection::Modernize,
286 title: "Modernize media range syntax",
287 category: "At-rules",
288 safety_level: SafetyLevel::FormattingOnly,
289 description: "Convert min/max-width and min/max-height media features to CSS Range Syntax (e.g. (width >= 800px)).",
290 },
291 RuleDefinition {
292 id: RuleId::MergeSameNamedLayer,
293 section: RuleSection::Refactor,
294 title: "Merge same named @layer blocks",
295 category: "Structural Refactoring",
296 safety_level: SafetyLevel::ProvenLocalRefactor,
297 description: "Consolidate separated blocks belonging to the same named @layer into their canonical first occurrence.",
298 },
299 RuleDefinition {
300 id: RuleId::MergeAdjacentMedia,
301 section: RuleSection::Refactor,
302 title: "Merge adjacent @media queries",
303 category: "Structural Refactoring",
304 safety_level: SafetyLevel::ProvenLocalRefactor,
305 description: "Combine consecutive @media blocks having identical query conditions into a single block.",
306 },
307 RuleDefinition {
308 id: RuleId::MergeAdjacentSupports,
309 section: RuleSection::Refactor,
310 title: "Merge adjacent @supports queries",
311 category: "Structural Refactoring",
312 safety_level: SafetyLevel::ProvenLocalRefactor,
313 description: "Combine consecutive @supports blocks having identical feature conditions into a single block.",
314 },
315 RuleDefinition {
316 id: RuleId::MergeAdjacentContainer,
317 section: RuleSection::Refactor,
318 title: "Merge adjacent @container queries",
319 category: "Structural Refactoring",
320 safety_level: SafetyLevel::ProvenLocalRefactor,
321 description: "Combine consecutive @container blocks having identical container name and query conditions.",
322 },
323 RuleDefinition {
324 id: RuleId::MergeIdenticalScope,
325 section: RuleSection::Refactor,
326 title: "Merge adjacent @scope blocks",
327 category: "Structural Refactoring",
328 safety_level: SafetyLevel::ProvenLocalRefactor,
329 description: "Combine consecutive @scope blocks having identical root and limit parameters.",
330 },
331 RuleDefinition {
332 id: RuleId::MergeIdenticalStartingStyle,
333 section: RuleSection::Refactor,
334 title: "Merge adjacent @starting-style blocks",
335 category: "Structural Refactoring",
336 safety_level: SafetyLevel::ProvenLocalRefactor,
337 description: "Combine consecutive top-level @starting-style blocks into a single block.",
338 },
339 RuleDefinition {
340 id: RuleId::MergeAdjacentIdenticalSelector,
341 section: RuleSection::Refactor,
342 title: "Merge adjacent identical selectors",
343 category: "Structural Refactoring",
344 safety_level: SafetyLevel::ProvenLocalRefactor,
345 description: "Combine consecutive style rules sharing the exact same selector when no intervening rules exist.",
346 },
347 RuleDefinition {
348 id: RuleId::MergeIdenticalRuleBodies,
349 section: RuleSection::Refactor,
350 title: "Merge identical rule bodies",
351 category: "Structural Refactoring",
352 safety_level: SafetyLevel::ProvenLocalRefactor,
353 description: "Combine selectors sharing identical declaration bodies into a unified comma-separated rule.",
354 },
355 RuleDefinition {
356 id: RuleId::FactorIdenticalStatesWithIs,
357 section: RuleSection::Refactor,
358 title: "Factor identical states with :is()",
359 category: "Structural Refactoring",
360 safety_level: SafetyLevel::ProvenLocalRefactor,
361 description: "Combine multiple states of the same element sharing identical bodies into &:is(:hover, :focus, ...) form.",
362 },
363 RuleDefinition {
364 id: RuleId::GatherRelatedSelectorRules,
365 section: RuleSection::Refactor,
366 title: "Gather related selector rules",
367 category: "Structural Refactoring",
368 safety_level: SafetyLevel::SemanticReview,
369 description: "Gather scattered related rules into the strongest existing parent (specificity wins; prefix nest beats appended `&` on a tie). Busy @media/@supports blocks with mixed selectors stay grouped. Review required for cascade crossing.",
370 },
371 RuleDefinition {
372 id: RuleId::PruneOverriddenDeclarations,
373 section: RuleSection::Refactor,
374 title: "Prune overridden declarations & rules",
375 category: "Structural Refactoring",
376 safety_level: SafetyLevel::ProvenLocalRefactor,
377 description: "Remove dead declarations and entire rules overridden by later identical selectors in the cascade.",
378 },
379 ]
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "kebab-case")]
384pub enum Preset {
385 Analysis,
386 Conservative,
387 Modern,
388 Refactor,
389 Aggressive,
390 Custom,
391}
392
393impl Preset {
394 pub const ALL: [Preset; 6] = [
395 Preset::Analysis,
396 Preset::Conservative,
397 Preset::Modern,
398 Preset::Refactor,
399 Preset::Aggressive,
400 Preset::Custom,
401 ];
402
403 pub const fn label(self) -> &'static str {
404 match self {
405 Self::Analysis => "Analysis",
406 Self::Conservative => "Conservative",
407 Self::Modern => "Modern",
408 Self::Refactor => "Refactor",
409 Self::Aggressive => "Aggressive",
410 Self::Custom => "Custom",
411 }
412 }
413
414 pub fn enabled_rules(self) -> Vec<RuleId> {
415 match self {
416 Self::Analysis => vec![],
417 Self::Conservative => vec![
418 RuleId::NestPseudoClass,
419 RuleId::NestPseudoElement,
420 RuleId::NestAttribute,
421 RuleId::NestCompound,
422 RuleId::NestDescendant,
423 RuleId::NestCombinator,
424 RuleId::NestMedia,
425 RuleId::NestSupports,
426 RuleId::NestStartingStyle,
427 RuleId::FactorSelectorList,
428 RuleId::ModernizeIs,
429 RuleId::ModernizeMediaRange,
430 RuleId::MergeSameNamedLayer,
431 RuleId::MergeAdjacentMedia,
432 RuleId::MergeAdjacentSupports,
433 RuleId::MergeAdjacentIdenticalSelector,
434 RuleId::MergeIdenticalRuleBodies,
435 RuleId::FactorIdenticalStatesWithIs,
436 ],
437 Self::Modern => vec![
438 RuleId::NestPseudoClass,
439 RuleId::NestPseudoElement,
440 RuleId::NestAttribute,
441 RuleId::NestCompound,
442 RuleId::NestDescendant,
443 RuleId::NestCombinator,
444 RuleId::NestMedia,
445 RuleId::NestSupports,
446 RuleId::NestContainer,
447 RuleId::NestStartingStyle,
448 RuleId::FactorSelectorList,
449 RuleId::ConsolidateNot,
450 RuleId::ModernizeIs,
451 RuleId::ModernizeMediaRange,
452 RuleId::PruneOverriddenDeclarations,
453 ],
454 Self::Refactor => vec![
455 RuleId::NestPseudoClass,
456 RuleId::NestPseudoElement,
457 RuleId::NestAttribute,
458 RuleId::NestCompound,
459 RuleId::NestDescendant,
460 RuleId::NestCombinator,
461 RuleId::NestMedia,
462 RuleId::NestSupports,
463 RuleId::NestContainer,
464 RuleId::NestStartingStyle,
465 RuleId::FactorSelectorList,
466 RuleId::ConsolidateNot,
467 RuleId::ModernizeIs,
468 RuleId::ModernizeMediaRange,
469 RuleId::MergeSameNamedLayer,
470 RuleId::MergeAdjacentMedia,
471 RuleId::MergeAdjacentSupports,
472 RuleId::MergeAdjacentContainer,
473 RuleId::MergeIdenticalScope,
474 RuleId::MergeIdenticalStartingStyle,
475 RuleId::MergeAdjacentIdenticalSelector,
476 RuleId::MergeIdenticalRuleBodies,
477 RuleId::FactorIdenticalStatesWithIs,
478 RuleId::GatherRelatedSelectorRules,
479 RuleId::PruneOverriddenDeclarations,
480 ],
481 Self::Aggressive => RuleId::ALL.to_vec(),
482 Self::Custom => vec![],
483 }
484 }
485}
486
487impl fmt::Display for Preset {
488 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489 f.write_str(self.label())
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
494#[serde(rename_all = "kebab-case")]
495pub enum OutputMode {
496 DryRun,
497 NewFile,
498 OutDir,
499 OverwriteWithBackup,
500 Overwrite,
501 Patch,
502 Stdout,
503}
504
505impl OutputMode {
506 pub const ALL: [OutputMode; 7] = [
507 Self::DryRun,
508 Self::NewFile,
509 Self::OutDir,
510 Self::OverwriteWithBackup,
511 Self::Overwrite,
512 Self::Patch,
513 Self::Stdout,
514 ];
515
516 pub const fn label(self) -> &'static str {
517 match self {
518 Self::DryRun => "Dry run",
519 Self::NewFile => "New file (*.modern.css)",
520 Self::OutDir => "Output directory",
521 Self::OverwriteWithBackup => "Overwrite + backup",
522 Self::Overwrite => "Overwrite",
523 Self::Patch => "Patch file",
524 Self::Stdout => "stdout",
525 }
526 }
527}
528
529impl fmt::Display for OutputMode {
530 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531 f.write_str(self.label())
532 }
533}
534
535#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
536pub struct SourceRange {
537 pub start: usize,
538 pub end: usize,
539}
540
541#[derive(Debug, Clone, Default, Serialize, Deserialize)]
542pub struct Proof {
543 pub selector_set_equivalent: bool,
544 pub specificity_equivalent: bool,
545 pub cascade_context_equivalent: bool,
546 pub source_order_equivalent: bool,
547 pub layer_equivalent: bool,
548 pub scope_equivalent: bool,
549 pub declarations_exact: bool,
550 pub important_exact: bool,
551}
552
553impl Proof {
554 pub fn safe_local() -> Self {
555 Self {
556 selector_set_equivalent: true,
557 specificity_equivalent: true,
558 cascade_context_equivalent: true,
559 source_order_equivalent: true,
560 layer_equivalent: true,
561 scope_equivalent: true,
562 declarations_exact: true,
563 important_exact: true,
564 }
565 }
566
567 pub fn all_pass(&self) -> bool {
568 self.selector_set_equivalent
569 && self.specificity_equivalent
570 && self.cascade_context_equivalent
571 && self.source_order_equivalent
572 && self.layer_equivalent
573 && self.scope_equivalent
574 && self.declarations_exact
575 && self.important_exact
576 }
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct PlanEntry {
581 pub id: String,
582 pub file: PathBuf,
583 pub rules: Vec<RuleId>,
584 pub safety: Safety,
585 pub source_range: SourceRange,
586 pub original: String,
587 pub proposed: String,
588 pub proof: Proof,
589 pub warnings: Vec<String>,
590 pub reason: String,
591 #[serde(default = "default_true")]
592 pub selected: bool,
593}
594
595fn default_true() -> bool {
596 true
597}
598
599#[derive(Debug, Clone, Default, Serialize, Deserialize)]
600pub struct AnalysisStats {
601 pub bytes: usize,
602 pub top_level_style_rules: usize,
603 pub top_level_at_rules: usize,
604 pub declarations: usize,
605 pub important_declarations: usize,
606 pub custom_properties: usize,
607 pub duplicate_selectors: usize,
608 pub media_rules: usize,
609 pub supports_rules: usize,
610 pub container_rules: usize,
611 pub layer_rules: usize,
612 pub scope_rules: usize,
613 pub starting_style_rules: usize,
614 pub parse_errors: usize,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct Finding {
619 pub safety: Safety,
620 pub title: String,
621 pub detail: String,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct FileReport {
626 pub path: PathBuf,
627 pub parse_ok: bool,
628 pub parse_error: Option<String>,
629 pub stats: AnalysisStats,
630 pub findings: Vec<Finding>,
631 pub plans: Vec<PlanEntry>,
632}
633
634#[derive(Debug, Clone, Default, Serialize, Deserialize)]
635pub struct WorkspaceSummary {
636 pub files: usize,
637 pub parse_errors: usize,
638 pub rules_analyzed: usize,
639 pub safe: usize,
640 pub review: usize,
641 pub unsafe_count: usize,
642 pub unsupported: usize,
643 pub no_op: usize,
644 pub specificity_sensitive: usize,
645 pub cascade_sensitive: usize,
646 pub layer_sensitive: usize,
647 pub scope_sensitive: usize,
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct WorkspaceReport {
652 pub tool_version: String,
653 pub spec_baseline: String,
654 pub root: PathBuf,
655 pub enabled_rules: Vec<RuleId>,
656 pub files: Vec<FileReport>,
657 pub summary: WorkspaceSummary,
658}