@canonical/code-standards 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/rust.md CHANGED
@@ -6,15 +6,9 @@ Standards for rust development.
6
6
 
7
7
  Use monadic Result chaining with the ? operator and combinators like and_then, map, and or_else for clean, composable error handling. This is the Rust equivalent of Haskell's do-notation for the Either monad.
8
8
 
9
- **Ratings:**
10
- - Impact: A (cleaner control flow, eliminates nested matches)
11
- - Feasibility: S (already natural in Rust)
12
- - Idiomaticity: S (idiomatic Rust; ? operator is standard)
13
- - FP Purity: S (direct monadic composition / Kleisli arrows)
14
-
15
9
  ### Do
16
10
 
17
- (Do) Use the ? operator for clean Result propagation.
11
+ Use the ? operator for clean Result propagation.
18
12
  ```rust
19
13
  fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
20
14
  let project_root = find_project_root()
@@ -38,7 +32,7 @@ fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
38
32
  }
39
33
  ```
40
34
 
41
- (Do) Use and_then for dependent computations.
35
+ Use and_then for dependent computations.
42
36
  ```rust
43
37
  fn process_user_input(input: &str) -> Result<Output, ProcessError> {
44
38
  parse_input(input)
@@ -48,7 +42,7 @@ fn process_user_input(input: &str) -> Result<Output, ProcessError> {
48
42
  }
49
43
  ```
50
44
 
51
- (Do) Use map for infallible transformations within Result.
45
+ Use map for infallible transformations within Result.
52
46
  ```rust
53
47
  fn get_package_names(path: &Path) -> Result<Vec<String>, IoError> {
54
48
  fs::read_dir(path)?
@@ -67,7 +61,7 @@ fn get_config_value(key: &str) -> Result<String, ConfigError> {
67
61
  }
68
62
  ```
69
63
 
70
- (Do) Use or_else for fallback computations.
64
+ Use or_else for fallback computations.
71
65
  ```rust
72
66
  fn find_config() -> Result<Config, ConfigError> {
73
67
  load_config_from_env()
@@ -76,7 +70,7 @@ fn find_config() -> Result<Config, ConfigError> {
76
70
  }
77
71
  ```
78
72
 
79
- (Do) Combine multiple Results with the ? operator in sequence.
73
+ Combine multiple Results with the ? operator in sequence.
80
74
  ```rust
81
75
  fn setup_application() -> Result<App, SetupError> {
82
76
  let config = load_config()?;
@@ -90,7 +84,7 @@ fn setup_application() -> Result<App, SetupError> {
90
84
 
91
85
  ### Don't
92
86
 
93
- (Don't) Use nested match expressions for Result handling.
87
+ Use nested match expressions for Result handling.
94
88
  ```rust
95
89
  // Bad: Deeply nested, hard to follow
96
90
  fn process(input: &str) -> Result<Output, Error> {
@@ -111,7 +105,7 @@ fn process(input: &str) -> Result<Output, Error> {
111
105
  }
112
106
  ```
113
107
 
114
- (Don't) Use unwrap() or expect() to bypass error handling.
108
+ Use unwrap() or expect() to bypass error handling.
115
109
  ```rust
116
110
  // Bad: Panics on error
117
111
  fn load_data() -> Data {
@@ -120,7 +114,7 @@ fn load_data() -> Data {
120
114
  }
121
115
  ```
122
116
 
123
- (Don't) Ignore errors silently.
117
+ Ignore errors silently.
124
118
  ```rust
125
119
  // Bad: Errors are silently dropped
126
120
  fn try_save(data: &Data) {
@@ -128,7 +122,7 @@ fn try_save(data: &Data) {
128
122
  }
129
123
  ```
130
124
 
131
- (Don't) Convert all errors to strings early in the chain.
125
+ Convert all errors to strings early in the chain.
132
126
  ```rust
133
127
  // Bad: Loses error type information
134
128
  fn process() -> Result<(), String> {
@@ -144,15 +138,9 @@ fn process() -> Result<(), String> {
144
138
 
145
139
  Always enrich errors with contextual information such as file paths, operation names, and relevant state. This dramatically improves debugging experience and follows the principle of errors as values.
146
140
 
147
- **Ratings:**
148
- - Impact: S (dramatically improves debugging experience)
149
- - Feasibility: A (thiserror + map_err pattern already established)
150
- - Idiomaticity: S (Rust community best practice)
151
- - FP Purity: B (error as value; monadic error propagation)
152
-
153
141
  ### Do
154
142
 
155
- (Do) Use thiserror to create structured, contextual error types.
143
+ Use thiserror to create structured, contextual error types.
156
144
  ```rust
157
145
  use thiserror::Error;
158
146
  use std::path::PathBuf;
@@ -183,7 +171,7 @@ pub enum GraphError {
183
171
  }
184
172
  ```
185
173
 
186
- (Do) Use map_err to add context when propagating errors.
174
+ Use map_err to add context when propagating errors.
187
175
  ```rust
188
176
  fn load_package(path: &Path) -> Result<Package, GraphError> {
189
177
  let content = fs::read_to_string(path)
@@ -202,7 +190,7 @@ fn load_package(path: &Path) -> Result<Package, GraphError> {
202
190
  }
203
191
  ```
204
192
 
205
- (Do) Chain errors to preserve the full error trail.
193
+ Chain errors to preserve the full error trail.
206
194
  ```rust
207
195
  #[derive(Error, Debug)]
208
196
  pub enum AppError {
@@ -221,7 +209,7 @@ pub enum AppError {
221
209
  }
222
210
  ```
223
211
 
224
- (Do) Include actionable information in error messages.
212
+ Include actionable information in error messages.
225
213
  ```rust
226
214
  #[derive(Error, Debug)]
227
215
  pub enum ValidationError {
@@ -235,7 +223,7 @@ pub enum ValidationError {
235
223
 
236
224
  ### Don't
237
225
 
238
- (Don't) Use generic string errors that lose context.
226
+ Use generic string errors that lose context.
239
227
  ```rust
240
228
  // Bad: Loses type information and context
241
229
  fn load_config(path: &Path) -> Result<Config, String> {
@@ -247,7 +235,7 @@ fn load_config(path: &Path) -> Result<Config, String> {
247
235
  }
248
236
  ```
249
237
 
250
- (Don't) Discard error information with unwrap or expect in library code.
238
+ Discard error information with unwrap or expect in library code.
251
239
  ```rust
252
240
  // Bad: Panics instead of propagating
253
241
  fn parse_uri(s: &str) -> Uri {
@@ -258,7 +246,7 @@ fn parse_uri(s: &str) -> Uri {
258
246
  let file = File::open(path).expect("failed"); // Which path?
259
247
  ```
260
248
 
261
- (Don't) Create error types without Display or Debug.
249
+ Create error types without Display or Debug.
262
250
  ```rust
263
251
  // Bad: Unusable error type
264
252
  pub struct MyError {
@@ -267,7 +255,7 @@ pub struct MyError {
267
255
  }
268
256
  ```
269
257
 
270
- (Don't) Use anyhow/eyre in library code (ok for applications).
258
+ Use anyhow/eyre in library code (ok for applications).
271
259
  ```rust
272
260
  // Bad in libraries: Erases type information
273
261
  pub fn process() -> anyhow::Result<()> {
@@ -286,15 +274,9 @@ pub fn process() -> Result<(), ProcessError> {
286
274
 
287
275
  Structure functions as `A -> Result<B, E>` (Kleisli arrows) for composable, chainable transformations. This enables powerful composition patterns where each step can fail, following the monadic composition style from Haskell.
288
276
 
289
- **Ratings:**
290
- - Impact: B (enables powerful composition patterns)
291
- - Feasibility: B (requires thinking in terms of monadic pipelines)
292
- - Idiomaticity: A (natural with ? and and_then)
293
- - FP Purity: S (direct Kleisli arrow pattern)
294
-
295
277
  ### Do
296
278
 
297
- (Do) Design functions as Kleisli arrows: `A -> Result<B, E>`.
279
+ Design functions as Kleisli arrows: `A -> Result<B, E>`.
298
280
  ```rust
299
281
  // Each function is a Kleisli arrow that can be composed
300
282
  fn parse_config(input: &str) -> Result<RawConfig, ParseError> { /* ... */ }
@@ -320,7 +302,7 @@ fn process_input(s: &str) -> Result<Output, ProcessError> {
320
302
  }
321
303
  ```
322
304
 
323
- (Do) Use combinators to compose fallible operations.
305
+ Use combinators to compose fallible operations.
324
306
  ```rust
325
307
  impl DepSpec {
326
308
  pub fn from_value(value: &DependencyValue) -> Result<Self, ParseError> {
@@ -340,7 +322,7 @@ impl DepSpec {
340
322
  }
341
323
  ```
342
324
 
343
- (Do) Create helper traits for method chaining when needed.
325
+ Create helper traits for method chaining when needed.
344
326
  ```rust
345
327
  trait ResultExt<T, E> {
346
328
  fn and_try<U, F>(self, f: F) -> Result<U, E>
@@ -364,7 +346,7 @@ let result = input
364
346
  .and_try(step3);
365
347
  ```
366
348
 
367
- (Do) Use the pipe pattern for readability.
349
+ Use the pipe pattern for readability.
368
350
  ```rust
369
351
  // With a pipe trait or tap crate
370
352
  trait Pipe: Sized {
@@ -383,7 +365,7 @@ let result = input
383
365
 
384
366
  ### Don't
385
367
 
386
- (Don't) Mix side effects into pure transformation chains.
368
+ Mix side effects into pure transformation chains.
387
369
  ```rust
388
370
  // Bad: Side effects hidden in chain
389
371
  fn process(input: &str) -> Result<Output, Error> {
@@ -406,7 +388,7 @@ fn process(input: &str) -> Result<Output, Error> {
406
388
  }
407
389
  ```
408
390
 
409
- (Don't) Break the chain with early returns when and_then works.
391
+ Break the chain with early returns when and_then works.
410
392
  ```rust
411
393
  // Bad: Breaks the monadic flow
412
394
  fn process(input: &str) -> Result<Output, Error> {
@@ -431,7 +413,7 @@ fn process(input: &str) -> Result<Output, Error> {
431
413
  }
432
414
  ```
433
415
 
434
- (Don't) Use unwrap in the middle of a chain.
416
+ Use unwrap in the middle of a chain.
435
417
  ```rust
436
418
  // Bad: Panics break the monadic abstraction
437
419
  let result = items.iter()
@@ -446,15 +428,9 @@ let result = items.iter()
446
428
 
447
429
  Prefer iterator combinators (map, filter, flat_map, collect) over imperative loops for data transformations. This functional style is more declarative, composable, and often more performant due to lazy evaluation and compiler optimizations.
448
430
 
449
- **Ratings:**
450
- - Impact: A (more declarative, composable, often faster)
451
- - Feasibility: A (Rust iterators are excellent)
452
- - Idiomaticity: S (core Rust idiom)
453
- - FP Purity: S (direct FP; lazy evaluation, composable)
454
-
455
431
  ### Do
456
432
 
457
- (Do) Use iterator chains for data transformations.
433
+ Use iterator chains for data transformations.
458
434
  ```rust
459
435
  fn get_installed_packages() -> Result<Vec<String>, IoError> {
460
436
  let packages_path = sem_packages_dir()?;
@@ -478,7 +454,7 @@ fn get_installed_packages() -> Result<Vec<String>, IoError> {
478
454
  }
479
455
  ```
480
456
 
481
- (Do) Use flat_map for one-to-many transformations.
457
+ Use flat_map for one-to-many transformations.
482
458
  ```rust
483
459
  fn get_all_dependencies(packages: &[Package]) -> Vec<Dependency> {
484
460
  packages.iter()
@@ -487,7 +463,7 @@ fn get_all_dependencies(packages: &[Package]) -> Vec<Dependency> {
487
463
  }
488
464
  ```
489
465
 
490
- (Do) Use filter_map to combine filter and map operations.
466
+ Use filter_map to combine filter and map operations.
491
467
  ```rust
492
468
  fn parse_valid_numbers(strings: &[&str]) -> Vec<i32> {
493
469
  strings.iter()
@@ -496,7 +472,7 @@ fn parse_valid_numbers(strings: &[&str]) -> Vec<i32> {
496
472
  }
497
473
  ```
498
474
 
499
- (Do) Use fold/reduce for accumulating results.
475
+ Use fold/reduce for accumulating results.
500
476
  ```rust
501
477
  fn total_size(files: &[PathBuf]) -> u64 {
502
478
  files.iter()
@@ -511,7 +487,7 @@ fn merge_configs(configs: &[Config]) -> Config {
511
487
  }
512
488
  ```
513
489
 
514
- (Do) Chain multiple operations for complex transformations.
490
+ Chain multiple operations for complex transformations.
515
491
  ```rust
516
492
  fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>> {
517
493
  entries.iter()
@@ -526,7 +502,7 @@ fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>>
526
502
  }
527
503
  ```
528
504
 
529
- (Do) Use collect with turbofish for type-driven collection.
505
+ Use collect with turbofish for type-driven collection.
530
506
  ```rust
531
507
  // Collect into different types based on need
532
508
  let vec: Vec<_> = iter.collect();
@@ -541,7 +517,7 @@ let results: Result<Vec<_>, _> = items.iter()
541
517
 
542
518
  ### Don't
543
519
 
544
- (Don't) Use imperative loops when combinators are clearer.
520
+ Use imperative loops when combinators are clearer.
545
521
  ```rust
546
522
  // Bad: Imperative style obscures intent
547
523
  fn get_names(users: &[User]) -> Vec<String> {
@@ -563,7 +539,7 @@ fn get_names(users: &[User]) -> Vec<String> {
563
539
  }
564
540
  ```
565
541
 
566
- (Don't) Collect intermediate results unnecessarily.
542
+ Collect intermediate results unnecessarily.
567
543
  ```rust
568
544
  // Bad: Unnecessary allocation
569
545
  let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
@@ -577,7 +553,7 @@ let result: i32 = items.iter()
577
553
  .sum();
578
554
  ```
579
555
 
580
- (Don't) Use for loops just to build up a Vec.
556
+ Use for loops just to build up a Vec.
581
557
  ```rust
582
558
  // Bad: Manual Vec building
583
559
  let mut results = Vec::new();
@@ -589,7 +565,7 @@ for item in items {
589
565
  let results: Vec<_> = items.iter().map(transform).collect();
590
566
  ```
591
567
 
592
- (Don't) Nest loops when flat_map works.
568
+ Nest loops when flat_map works.
593
569
  ```rust
594
570
  // Bad: Nested loops
595
571
  let mut all_items = Vec::new();
@@ -611,15 +587,9 @@ let all_items: Vec<_> = containers.iter()
611
587
 
612
588
  Use Option explicitly and idiomatically; prefer Option<T> combinators (map, and_then, unwrap_or, ok_or) over null-like patterns. Option is Rust's Maybe monad - use it as such.
613
589
 
614
- **Ratings:**
615
- - Impact: A (eliminates null-related bugs)
616
- - Feasibility: S (Rust enforces this)
617
- - Idiomaticity: S (fundamental Rust pattern)
618
- - FP Purity: S (Maybe monad equivalent)
619
-
620
590
  ### Do
621
591
 
622
- (Do) Use Option combinators for clean transformations.
592
+ Use Option combinators for clean transformations.
623
593
  ```rust
624
594
  impl Manifest {
625
595
  pub fn display_name(&self) -> Option<String> {
@@ -638,7 +608,7 @@ impl Manifest {
638
608
  }
639
609
  ```
640
610
 
641
- (Do) Use unwrap_or and unwrap_or_else for defaults.
611
+ Use unwrap_or and unwrap_or_else for defaults.
642
612
  ```rust
643
613
  fn get_config_value(key: &str) -> String {
644
614
  config.get(key)
@@ -654,7 +624,7 @@ fn get_timeout() -> Duration {
654
624
  }
655
625
  ```
656
626
 
657
- (Do) Use ok_or to convert Option to Result.
627
+ Use ok_or to convert Option to Result.
658
628
  ```rust
659
629
  fn find_package(name: &str) -> Result<&Package, PackageError> {
660
630
  packages.get(name)
@@ -667,7 +637,7 @@ fn get_required_field<'a>(map: &'a HashMap<String, Value>, key: &str) -> Result<
667
637
  }
668
638
  ```
669
639
 
670
- (Do) Use filter and filter_map for conditional processing.
640
+ Use filter and filter_map for conditional processing.
671
641
  ```rust
672
642
  fn find_active_user(id: UserId) -> Option<User> {
673
643
  users.get(&id).filter(|u| u.is_active)
@@ -680,7 +650,7 @@ fn get_valid_entries(entries: &[Entry]) -> Vec<&Entry> {
680
650
  }
681
651
  ```
682
652
 
683
- (Do) Use the ? operator with Option in functions returning Option.
653
+ Use the ? operator with Option in functions returning Option.
684
654
  ```rust
685
655
  fn get_nested_value(data: &Data) -> Option<&str> {
686
656
  let section = data.sections.get("main")?;
@@ -691,7 +661,7 @@ fn get_nested_value(data: &Data) -> Option<&str> {
691
661
 
692
662
  ### Don't
693
663
 
694
- (Don't) Use sentinel values instead of Option.
664
+ Use sentinel values instead of Option.
695
665
  ```rust
696
666
  // Bad: Magic values
697
667
  fn find_index(items: &[Item], target: &Item) -> i32 {
@@ -705,7 +675,7 @@ fn find_index(items: &[Item], target: &Item) -> Option<usize> {
705
675
  }
706
676
  ```
707
677
 
708
- (Don't) Overuse unwrap() or expect() outside of tests.
678
+ Overuse unwrap() or expect() outside of tests.
709
679
  ```rust
710
680
  // Bad: Panics on None
711
681
  let user = users.get(id).unwrap();
@@ -716,7 +686,7 @@ let user = users.get(id).ok_or(UserError::NotFound(id))?;
716
686
  let name = user.name.as_deref().unwrap_or("Anonymous");
717
687
  ```
718
688
 
719
- (Don't) Check is_some/is_none then unwrap.
689
+ Check is_some/is_none then unwrap.
720
690
  ```rust
721
691
  // Bad: Redundant check
722
692
  if value.is_some() {
@@ -733,7 +703,7 @@ if let Some(v) = value {
733
703
  value.map(process);
734
704
  ```
735
705
 
736
- (Don't) Create deeply nested Option chains without combinators.
706
+ Create deeply nested Option chains without combinators.
737
707
  ```rust
738
708
  // Bad: Nested matching
739
709
  match outer {
@@ -760,15 +730,9 @@ outer
760
730
 
761
731
  Complement unit tests with property-based testing to verify invariants across many generated inputs. This QuickCheck-inspired approach catches edge cases that example-based tests miss.
762
732
 
763
- **Ratings:**
764
- - Impact: A (catches edge cases unit tests miss)
765
- - Feasibility: B (requires proptest/quickcheck; learning curve)
766
- - Idiomaticity: A (growing Rust practice)
767
- - FP Purity: S (QuickCheck heritage; declarative testing)
768
-
769
733
  ### Do
770
734
 
771
- (Do) Use proptest for property-based testing.
735
+ Use proptest for property-based testing.
772
736
  ```rust
773
737
  use proptest::prelude::*;
774
738
 
@@ -794,7 +758,7 @@ proptest! {
794
758
  }
795
759
  ```
796
760
 
797
- (Do) Test algebraic properties (identity, associativity, commutativity).
761
+ Test algebraic properties (identity, associativity, commutativity).
798
762
  ```rust
799
763
  proptest! {
800
764
  #[test]
@@ -814,7 +778,7 @@ proptest! {
814
778
  }
815
779
  ```
816
780
 
817
- (Do) Test invariants that should hold for all valid inputs.
781
+ Test invariants that should hold for all valid inputs.
818
782
  ```rust
819
783
  proptest! {
820
784
  #[test]
@@ -838,7 +802,7 @@ proptest! {
838
802
  }
839
803
  ```
840
804
 
841
- (Do) Use custom strategies for domain-specific types.
805
+ Use custom strategies for domain-specific types.
842
806
  ```rust
843
807
  fn valid_package_name() -> impl Strategy<Value = String> {
844
808
  "[a-z][a-z0-9-]{0,62}[a-z0-9]?"
@@ -860,7 +824,7 @@ proptest! {
860
824
 
861
825
  ### Don't
862
826
 
863
- (Don't) Only write example-based unit tests for complex logic.
827
+ Only write example-based unit tests for complex logic.
864
828
  ```rust
865
829
  // Incomplete: Only tests a few examples
866
830
  #[test]
@@ -873,7 +837,7 @@ fn test_merge() {
873
837
  }
874
838
  ```
875
839
 
876
- (Don't) Ignore test failures without understanding the counterexample.
840
+ Ignore test failures without understanding the counterexample.
877
841
  ```rust
878
842
  // Bad: Ignoring failures
879
843
  proptest! {
@@ -885,7 +849,7 @@ proptest! {
885
849
  }
886
850
  ```
887
851
 
888
- (Don't) Write properties that are too weak or tautological.
852
+ Write properties that are too weak or tautological.
889
853
  ```rust
890
854
  // Bad: This always passes, tests nothing useful
891
855
  proptest! {
@@ -905,7 +869,7 @@ proptest! {
905
869
  }
906
870
  ```
907
871
 
908
- (Don't) Generate invalid inputs without proper filtering.
872
+ Generate invalid inputs without proper filtering.
909
873
  ```rust
910
874
  // Bad: Generates invalid UTF-8 and panics
911
875
  proptest! {
@@ -930,15 +894,9 @@ proptest! {
930
894
 
931
895
  Follow the strict TDD cycle: write a failing test first (red), implement minimally to pass (green), then refactor. This discipline ensures testability, prevents regression, and drives better design.
932
896
 
933
- **Ratings:**
934
- - Impact: S (ensures testability, prevents regression)
935
- - Feasibility: A (Rust's cargo test makes this easy)
936
- - Idiomaticity: A (industry best practice)
937
- - FP Purity: B (supports referential transparency goals)
938
-
939
897
  ### Do
940
898
 
941
- (Do) Write the test first, watch it fail.
899
+ Write the test first, watch it fail.
942
900
  ```rust
943
901
  // Step 1: RED - Write failing test
944
902
  #[cfg(test)]
@@ -957,7 +915,7 @@ mod tests {
957
915
  // At this point: cargo test fails - Version doesn't exist yet!
958
916
  ```
959
917
 
960
- (Do) Implement the minimum code to pass.
918
+ Implement the minimum code to pass.
961
919
  ```rust
962
920
  // Step 2: GREEN - Minimal implementation
963
921
  pub struct Version {
@@ -983,7 +941,7 @@ impl Version {
983
941
  // cargo test passes!
984
942
  ```
985
943
 
986
- (Do) Refactor while keeping tests green.
944
+ Refactor while keeping tests green.
987
945
  ```rust
988
946
  // Step 3: REFACTOR - Improve design
989
947
  impl Version {
@@ -1011,7 +969,7 @@ impl Version {
1011
969
  // Tests still pass after refactoring!
1012
970
  ```
1013
971
 
1014
- (Do) Add tests for edge cases incrementally.
972
+ Add tests for edge cases incrementally.
1015
973
  ```rust
1016
974
  #[test]
1017
975
  fn parse_rejects_empty_string() {
@@ -1035,7 +993,7 @@ fn parse_handles_leading_zeros() {
1035
993
  }
1036
994
  ```
1037
995
 
1038
- (Do) Use test modules colocated with implementation.
996
+ Use test modules colocated with implementation.
1039
997
  ```rust
1040
998
  // src/version.rs
1041
999
  pub struct Version { /* ... */ }
@@ -1056,7 +1014,7 @@ mod tests {
1056
1014
 
1057
1015
  ### Don't
1058
1016
 
1059
- (Don't) Write implementation before tests.
1017
+ Write implementation before tests.
1060
1018
  ```rust
1061
1019
  // Bad: Implementation without tests
1062
1020
  pub fn complex_algorithm(input: &str) -> Result<Output, Error> {
@@ -1066,7 +1024,7 @@ pub fn complex_algorithm(input: &str) -> Result<Output, Error> {
1066
1024
  }
1067
1025
  ```
1068
1026
 
1069
- (Don't) Write tests that pass trivially or test nothing.
1027
+ Write tests that pass trivially or test nothing.
1070
1028
  ```rust
1071
1029
  // Bad: Test that always passes
1072
1030
  #[test]
@@ -1081,7 +1039,7 @@ fn test_without_assertions() {
1081
1039
  }
1082
1040
  ```
1083
1041
 
1084
- (Don't) Skip the refactor step.
1042
+ Skip the refactor step.
1085
1043
  ```rust
1086
1044
  // Bad: "It works, ship it!"
1087
1045
  impl Version {
@@ -1099,7 +1057,7 @@ impl Version {
1099
1057
  }
1100
1058
  ```
1101
1059
 
1102
- (Don't) Write tests after the fact that just confirm current behavior.
1060
+ Write tests after the fact that just confirm current behavior.
1103
1061
  ```rust
1104
1062
  // Bad: "Characterization tests" without understanding intent
1105
1063
  #[test]
@@ -1109,7 +1067,7 @@ fn test_weird_behavior() {
1109
1067
  }
1110
1068
  ```
1111
1069
 
1112
- (Don't) Test private implementation details.
1070
+ Test private implementation details.
1113
1071
  ```rust
1114
1072
  // Bad: Testing internals that may change
1115
1073
  #[test]
@@ -1135,15 +1093,9 @@ fn test_caching_behavior() {
1135
1093
 
1136
1094
  Prefer small, focused traits that compose well over large monolithic interfaces. This Haskell typeclass-inspired approach enables better abstraction, easier testing, and more flexible code reuse.
1137
1095
 
1138
- **Ratings:**
1139
- - Impact: A (better abstraction, easier testing)
1140
- - Feasibility: B (requires careful API design)
1141
- - Idiomaticity: S (Rust's trait system shines here)
1142
- - FP Purity: A (typeclass-inspired composition)
1143
-
1144
1096
  ### Do
1145
1097
 
1146
- (Do) Design small, single-purpose traits.
1098
+ Design small, single-purpose traits.
1147
1099
  ```rust
1148
1100
  /// Can be resolved from a prefixed form to a full URI
1149
1101
  trait Resolvable {
@@ -1168,7 +1120,7 @@ trait Loadable: Sized {
1168
1120
  }
1169
1121
  ```
1170
1122
 
1171
- (Do) Compose traits using supertraits and bounds.
1123
+ Compose traits using supertraits and bounds.
1172
1124
  ```rust
1173
1125
  // Compose small traits into larger capabilities
1174
1126
  trait UriHandler: Resolvable + Compactable {}
@@ -1182,7 +1134,7 @@ fn process_uri<T: Resolvable + Display>(uri: &T, map: &PrefixMap) -> String {
1182
1134
  }
1183
1135
  ```
1184
1136
 
1185
- (Do) Use extension traits to add methods to existing types.
1137
+ Use extension traits to add methods to existing types.
1186
1138
  ```rust
1187
1139
  trait ResultExt<T, E> {
1188
1140
  fn context(self, msg: &str) -> Result<T, ContextError<E>>;
@@ -1204,7 +1156,7 @@ let data = fs::read_to_string(path)
1204
1156
  .context("failed to read config")?;
1205
1157
  ```
1206
1158
 
1207
- (Do) Implement standard library traits for interoperability.
1159
+ Implement standard library traits for interoperability.
1208
1160
  ```rust
1209
1161
  impl Default for SemStore {
1210
1162
  fn default() -> Self {
@@ -1229,7 +1181,7 @@ impl FromStr for Version {
1229
1181
 
1230
1182
  ### Don't
1231
1183
 
1232
- (Don't) Create large, monolithic traits.
1184
+ Create large, monolithic traits.
1233
1185
  ```rust
1234
1186
  // Bad: Too many responsibilities
1235
1187
  trait Repository {
@@ -1247,7 +1199,7 @@ trait Repository {
1247
1199
  }
1248
1200
  ```
1249
1201
 
1250
- (Don't) Use trait objects when generics suffice.
1202
+ Use trait objects when generics suffice.
1251
1203
  ```rust
1252
1204
  // Bad: Unnecessary dynamic dispatch
1253
1205
  fn process(items: &[Box<dyn Processable>]) {
@@ -1264,7 +1216,7 @@ fn process<T: Processable>(items: &[T]) {
1264
1216
  }
1265
1217
  ```
1266
1218
 
1267
- (Don't) Require unused trait methods via blanket requirements.
1219
+ Require unused trait methods via blanket requirements.
1268
1220
  ```rust
1269
1221
  // Bad: Forces implementers to provide unused methods
1270
1222
  trait DataStore: Connect + Query + Mutate + Transaction + Cache + Log {
@@ -1277,7 +1229,7 @@ fn process<T: Query + Mutate>(store: &mut T) {
1277
1229
  }
1278
1230
  ```
1279
1231
 
1280
- (Don't) Use associated types when generic parameters work better.
1232
+ Use associated types when generic parameters work better.
1281
1233
  ```rust
1282
1234
  // Bad: Can only have one implementation per type
1283
1235
  trait Container {
@@ -1298,15 +1250,9 @@ trait Container<T> {
1298
1250
 
1299
1251
  Model state and variants with enums (algebraic sum types) rather than flags, inheritance, or stringly-typed values. Exhaustive pattern matching ensures all cases are handled at compile time - Rust's killer feature borrowed from ML/Haskell.
1300
1252
 
1301
- **Ratings:**
1302
- - Impact: S (exhaustive matching prevents bugs)
1303
- - Feasibility: A (native Rust feature)
1304
- - Idiomaticity: S (Rust's killer feature)
1305
- - FP Purity: S (algebraic data types; Haskell-equivalent)
1306
-
1307
1253
  ### Do
1308
1254
 
1309
- (Do) Use enums to model mutually exclusive states.
1255
+ Use enums to model mutually exclusive states.
1310
1256
  ```rust
1311
1257
  #[derive(Debug, Clone, PartialEq)]
1312
1258
  pub enum DepSpec {
@@ -1333,7 +1279,7 @@ impl DepSpec {
1333
1279
  }
1334
1280
  ```
1335
1281
 
1336
- (Do) Use enums for state machines with compile-time guarantees.
1282
+ Use enums for state machines with compile-time guarantees.
1337
1283
  ```rust
1338
1284
  enum ConnectionState {
1339
1285
  Disconnected,
@@ -1363,7 +1309,7 @@ impl ConnectionState {
1363
1309
  }
1364
1310
  ```
1365
1311
 
1366
- (Do) Use enums to make invalid states unrepresentable.
1312
+ Use enums to make invalid states unrepresentable.
1367
1313
  ```rust
1368
1314
  // User can be either anonymous or authenticated, never both
1369
1315
  enum User {
@@ -1383,7 +1329,7 @@ enum FieldState<T> {
1383
1329
  }
1384
1330
  ```
1385
1331
 
1386
- (Do) Leverage exhaustive matching for safety.
1332
+ Leverage exhaustive matching for safety.
1387
1333
  ```rust
1388
1334
  fn handle_result(result: QueryResult) -> Response {
1389
1335
  match result {
@@ -1400,7 +1346,7 @@ fn handle_result(result: QueryResult) -> Response {
1400
1346
 
1401
1347
  ### Don't
1402
1348
 
1403
- (Don't) Use boolean flags for mutually exclusive states.
1349
+ Use boolean flags for mutually exclusive states.
1404
1350
  ```rust
1405
1351
  // Bad: Multiple bools can have invalid combinations
1406
1352
  struct User {
@@ -1415,7 +1361,7 @@ struct Connection {
1415
1361
  }
1416
1362
  ```
1417
1363
 
1418
- (Don't) Use Option when you need more than two states.
1364
+ Use Option when you need more than two states.
1419
1365
  ```rust
1420
1366
  // Bad: Option doesn't capture "loading" vs "error" vs "empty"
1421
1367
  struct DataView {
@@ -1431,7 +1377,7 @@ enum DataState {
1431
1377
  }
1432
1378
  ```
1433
1379
 
1434
- (Don't) Use inheritance-like patterns with trait objects when enums suffice.
1380
+ Use inheritance-like patterns with trait objects when enums suffice.
1435
1381
  ```rust
1436
1382
  // Bad: Runtime dispatch when compile-time would work
1437
1383
  trait Shape {
@@ -1456,7 +1402,7 @@ impl Shape {
1456
1402
  }
1457
1403
  ```
1458
1404
 
1459
- (Don't) Use integers or strings as type discriminators.
1405
+ Use integers or strings as type discriminators.
1460
1406
  ```rust
1461
1407
  // Bad: Magic numbers
1462
1408
  const USER_TYPE_ADMIN: i32 = 1;
@@ -1473,15 +1419,9 @@ struct Message { msg_type: String } // "request", "response", typos!
1473
1419
 
1474
1420
  Use the newtype pattern to create distinct types for domain-specific values, preventing accidental mixing of semantically different data. This Haskell-inspired pattern provides compile-time safety with zero runtime cost.
1475
1421
 
1476
- **Ratings:**
1477
- - Impact: A (prevents mixing semantically different values)
1478
- - Feasibility: B (requires discipline; some boilerplate)
1479
- - Idiomaticity: S (core Rust pattern, zero-cost abstraction)
1480
- - FP Purity: A (Haskell-inspired; phantom types possible)
1481
-
1482
1422
  ### Do
1483
1423
 
1484
- (Do) Wrap primitive types in newtypes for domain semantics.
1424
+ Wrap primitive types in newtypes for domain semantics.
1485
1425
  ```rust
1486
1426
  /// A validated package name following sem conventions
1487
1427
  #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -1505,7 +1445,7 @@ impl PackageName {
1505
1445
  }
1506
1446
  ```
1507
1447
 
1508
- (Do) Use newtypes to distinguish semantically different IDs.
1448
+ Use newtypes to distinguish semantically different IDs.
1509
1449
  ```rust
1510
1450
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1511
1451
  pub struct UserId(u64);
@@ -1520,14 +1460,14 @@ fn process_order(user: UserId, order: OrderId) { /* ... */ }
1520
1460
  // process_order(order_id, user_id); // Error: expected UserId, found OrderId
1521
1461
  ```
1522
1462
 
1523
- (Do) Derive common traits to maintain ergonomics.
1463
+ Derive common traits to maintain ergonomics.
1524
1464
  ```rust
1525
1465
  #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1526
1466
  #[serde(transparent)]
1527
1467
  pub struct Uri(String);
1528
1468
  ```
1529
1469
 
1530
- (Do) Use phantom types for compile-time state tracking.
1470
+ Use phantom types for compile-time state tracking.
1531
1471
  ```rust
1532
1472
  use std::marker::PhantomData;
1533
1473
 
@@ -1555,7 +1495,7 @@ impl Input<Validated> {
1555
1495
 
1556
1496
  ### Don't
1557
1497
 
1558
- (Don't) Use raw primitive types for domain concepts.
1498
+ Use raw primitive types for domain concepts.
1559
1499
  ```rust
1560
1500
  // Bad: Raw strings lose semantic meaning
1561
1501
  fn load_package(name: String, path: String, uri: String) {
@@ -1566,7 +1506,7 @@ fn load_package(name: String, path: String, uri: String) {
1566
1506
  load_package(uri, name, path); // Compiles but wrong!
1567
1507
  ```
1568
1508
 
1569
- (Don't) Create newtypes without validation when invariants exist.
1509
+ Create newtypes without validation when invariants exist.
1570
1510
  ```rust
1571
1511
  // Bad: Allows invalid state
1572
1512
  pub struct Email(pub String); // pub field allows any string
@@ -1584,7 +1524,7 @@ impl Email {
1584
1524
  }
1585
1525
  ```
1586
1526
 
1587
- (Don't) Over-wrap types that don't need semantic distinction.
1527
+ Over-wrap types that don't need semantic distinction.
1588
1528
  ```rust
1589
1529
  // Bad: Unnecessary wrapping of implementation details
1590
1530
  struct LoopCounter(usize); // Just use usize
@@ -1597,15 +1537,9 @@ struct TempBuffer(Vec<u8>); // Just use Vec<u8>
1597
1537
 
1598
1538
  Use constructor validation to ensure only valid states can exist. Follow the 'parse, don't validate' principle: transform unvalidated data into validated types at system boundaries, making invalid states unrepresentable.
1599
1539
 
1600
- **Ratings:**
1601
- - Impact: S (invalid states become unrepresentable)
1602
- - Feasibility: B (requires upfront design thinking)
1603
- - Idiomaticity: A (recommended Rust pattern)
1604
- - FP Purity: A (Haskell-inspired 'make illegal states unrepresentable')
1605
-
1606
1540
  ### Do
1607
1541
 
1608
- (Do) Validate in constructors to ensure type invariants.
1542
+ Validate in constructors to ensure type invariants.
1609
1543
  ```rust
1610
1544
  impl Manifest {
1611
1545
  pub fn from_path(path: &Path) -> Result<Self, ManifestError> {
@@ -1631,7 +1565,7 @@ impl Manifest {
1631
1565
  }
1632
1566
  ```
1633
1567
 
1634
- (Do) Use the typestate pattern for compile-time state enforcement.
1568
+ Use the typestate pattern for compile-time state enforcement.
1635
1569
  ```rust
1636
1570
  // States as zero-sized types
1637
1571
  struct Draft;
@@ -1670,7 +1604,7 @@ impl Article<Published> {
1670
1604
  // article_published.publish(); // Error: method not found
1671
1605
  ```
1672
1606
 
1673
- (Do) Parse into validated types at system boundaries.
1607
+ Parse into validated types at system boundaries.
1674
1608
  ```rust
1675
1609
  // Raw input from external source
1676
1610
  struct RawUserInput {
@@ -1705,7 +1639,7 @@ impl Email {
1705
1639
  }
1706
1640
  ```
1707
1641
 
1708
- (Do) Make the validated state obvious in function signatures.
1642
+ Make the validated state obvious in function signatures.
1709
1643
  ```rust
1710
1644
  // Functions that require validation communicate it via types
1711
1645
  fn send_email(to: &Email, subject: &str, body: &str) -> Result<(), SendError> {
@@ -1719,7 +1653,7 @@ fn create_account(user: ValidatedUser) -> Result<Account, AccountError> {
1719
1653
 
1720
1654
  ### Don't
1721
1655
 
1722
- (Don't) Scatter validation logic throughout the codebase.
1656
+ Scatter validation logic throughout the codebase.
1723
1657
  ```rust
1724
1658
  // Bad: Validation repeated everywhere
1725
1659
  fn send_email(to: &str, subject: &str, body: &str) -> Result<(), Error> {
@@ -1737,7 +1671,7 @@ fn save_user(email: &str) -> Result<(), Error> {
1737
1671
  }
1738
1672
  ```
1739
1673
 
1740
- (Don't) Allow construction of invalid objects.
1674
+ Allow construction of invalid objects.
1741
1675
  ```rust
1742
1676
  // Bad: Public fields allow invalid state
1743
1677
  pub struct Email {
@@ -1752,7 +1686,7 @@ impl User {
1752
1686
  }
1753
1687
  ```
1754
1688
 
1755
- (Don't) Use validation functions that return bool.
1689
+ Use validation functions that return bool.
1756
1690
  ```rust
1757
1691
  // Bad: Caller can ignore the result
1758
1692
  fn is_valid_email(s: &str) -> bool {
@@ -1769,7 +1703,7 @@ fn parse_email(s: &str) -> Result<Email, ValidationError>
1769
1703
  // Caller must handle the Result
1770
1704
  ```
1771
1705
 
1772
- (Don't) Re-validate already-validated data.
1706
+ Re-validate already-validated data.
1773
1707
  ```rust
1774
1708
  // Bad: Redundant validation
1775
1709
  fn process(user: ValidatedUser) -> Result<(), Error> {