@canonical/code-standards 0.1.0 → 0.1.2
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/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/workflows/ci.yml +40 -0
- package/biome.json +6 -0
- package/bun.lock +200 -0
- package/data/code.ttl +208 -167
- package/data/css.ttl +110 -91
- package/data/icons.ttl +186 -150
- package/data/packaging.ttl +428 -170
- package/data/react.ttl +306 -244
- package/data/rust.ttl +563 -467
- package/data/storybook.ttl +108 -90
- package/data/styling.ttl +40 -40
- package/data/tsdoc.ttl +111 -86
- package/data/turtle.ttl +89 -68
- package/definitions/CodeStandard.ttl +28 -20
- package/docs/code.md +37 -327
- package/docs/css.md +24 -20
- package/docs/icons.md +41 -42
- package/docs/index.md +2 -1
- package/docs/packaging.md +643 -0
- package/docs/react.md +58 -59
- package/docs/rust.md +92 -158
- package/docs/storybook.md +18 -20
- package/docs/styling.md +8 -8
- package/docs/tsdoc.md +16 -16
- package/docs/turtle.md +15 -15
- package/package.json +16 -2
- package/skills/add-standard/SKILL.md +83 -47
- package/src/scripts/generate-docs.ts +95 -13
- package/src/scripts/index.ts +4 -2
- package/tsconfig.json +8 -0
package/data/rust.ttl
CHANGED
|
@@ -20,16 +20,11 @@ cs:RustCategory a cs:Category ;
|
|
|
20
20
|
cs:NewtypeWrappers a cs:CodeStandard ;
|
|
21
21
|
cs:name "rust/types/newtype-wrappers" ;
|
|
22
22
|
cs:hasCategory cs:RustCategory ;
|
|
23
|
-
cs:description """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.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- Idiomaticity: S (core Rust pattern, zero-cost abstraction)
|
|
29
|
-
- FP Purity: A (Haskell-inspired; phantom types possible)""" ;
|
|
30
|
-
cs:dos """
|
|
31
|
-
(Do) Wrap primitive types in newtypes for domain semantics.
|
|
32
|
-
```rust
|
|
23
|
+
cs:description """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.""" ;
|
|
24
|
+
cs:do [
|
|
25
|
+
cs:description "Wrap primitive types in newtypes for domain semantics." ;
|
|
26
|
+
cs:language "rust" ;
|
|
27
|
+
cs:code """
|
|
33
28
|
/// A validated package name following sem conventions
|
|
34
29
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
35
30
|
pub struct PackageName(String);
|
|
@@ -50,10 +45,12 @@ impl PackageName {
|
|
|
50
45
|
&self.0
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
"""
|
|
49
|
+
] ;
|
|
50
|
+
cs:do [
|
|
51
|
+
cs:description "Use newtypes to distinguish semantically different IDs." ;
|
|
52
|
+
cs:language "rust" ;
|
|
53
|
+
cs:code """
|
|
57
54
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
58
55
|
pub struct UserId(u64);
|
|
59
56
|
|
|
@@ -65,17 +62,21 @@ fn process_order(user: UserId, order: OrderId) { /* ... */ }
|
|
|
65
62
|
|
|
66
63
|
// This won't compile:
|
|
67
64
|
// process_order(order_id, user_id); // Error: expected UserId, found OrderId
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
"""
|
|
66
|
+
] ;
|
|
67
|
+
cs:do [
|
|
68
|
+
cs:description "Derive common traits to maintain ergonomics." ;
|
|
69
|
+
cs:language "rust" ;
|
|
70
|
+
cs:code """
|
|
72
71
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
73
72
|
#[serde(transparent)]
|
|
74
73
|
pub struct Uri(String);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
"""
|
|
75
|
+
] ;
|
|
76
|
+
cs:do [
|
|
77
|
+
cs:description "Use phantom types for compile-time state tracking." ;
|
|
78
|
+
cs:language "rust" ;
|
|
79
|
+
cs:code """
|
|
79
80
|
use std::marker::PhantomData;
|
|
80
81
|
|
|
81
82
|
struct Validated;
|
|
@@ -98,11 +99,12 @@ impl Input<Validated> {
|
|
|
98
99
|
// Only validated inputs can be processed
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
cs:
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
"""
|
|
103
|
+
] ;
|
|
104
|
+
cs:dont [
|
|
105
|
+
cs:description "Use raw primitive types for domain concepts." ;
|
|
106
|
+
cs:language "rust" ;
|
|
107
|
+
cs:code """
|
|
106
108
|
// Bad: Raw strings lose semantic meaning
|
|
107
109
|
fn load_package(name: String, path: String, uri: String) {
|
|
108
110
|
// Easy to mix up arguments - compiler can't help
|
|
@@ -110,10 +112,12 @@ fn load_package(name: String, path: String, uri: String) {
|
|
|
110
112
|
|
|
111
113
|
// Bad: Easy to accidentally swap arguments
|
|
112
114
|
load_package(uri, name, path); // Compiles but wrong!
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
"""
|
|
116
|
+
] ;
|
|
117
|
+
cs:dont [
|
|
118
|
+
cs:description "Create newtypes without validation when invariants exist." ;
|
|
119
|
+
cs:language "rust" ;
|
|
120
|
+
cs:code """
|
|
117
121
|
// Bad: Allows invalid state
|
|
118
122
|
pub struct Email(pub String); // pub field allows any string
|
|
119
123
|
|
|
@@ -128,15 +132,17 @@ impl Email {
|
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
"""
|
|
136
|
+
] ;
|
|
137
|
+
cs:dont [
|
|
138
|
+
cs:description "Over-wrap types that don't need semantic distinction." ;
|
|
139
|
+
cs:language "rust" ;
|
|
140
|
+
cs:code """
|
|
135
141
|
// Bad: Unnecessary wrapping of implementation details
|
|
136
142
|
struct LoopCounter(usize); // Just use usize
|
|
137
143
|
struct TempBuffer(Vec<u8>); // Just use Vec<u8>
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
"""
|
|
145
|
+
] .
|
|
140
146
|
|
|
141
147
|
# =============================================================================
|
|
142
148
|
# Standard 2: Error Context Enrichment
|
|
@@ -145,16 +151,11 @@ struct TempBuffer(Vec<u8>); // Just use Vec<u8>
|
|
|
145
151
|
cs:ErrorContextEnrichment a cs:CodeStandard ;
|
|
146
152
|
cs:name "rust/errors/context-enrichment" ;
|
|
147
153
|
cs:hasCategory cs:RustCategory ;
|
|
148
|
-
cs:description """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.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- Idiomaticity: S (Rust community best practice)
|
|
154
|
-
- FP Purity: B (error as value; monadic error propagation)""" ;
|
|
155
|
-
cs:dos """
|
|
156
|
-
(Do) Use thiserror to create structured, contextual error types.
|
|
157
|
-
```rust
|
|
154
|
+
cs:description """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.""" ;
|
|
155
|
+
cs:do [
|
|
156
|
+
cs:description "Use thiserror to create structured, contextual error types." ;
|
|
157
|
+
cs:language "rust" ;
|
|
158
|
+
cs:code """
|
|
158
159
|
use thiserror::Error;
|
|
159
160
|
use std::path::PathBuf;
|
|
160
161
|
|
|
@@ -182,10 +183,12 @@ pub enum GraphError {
|
|
|
182
183
|
message: String,
|
|
183
184
|
},
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
"""
|
|
187
|
+
] ;
|
|
188
|
+
cs:do [
|
|
189
|
+
cs:description "Use map_err to add context when propagating errors." ;
|
|
190
|
+
cs:language "rust" ;
|
|
191
|
+
cs:code """
|
|
189
192
|
fn load_package(path: &Path) -> Result<Package, GraphError> {
|
|
190
193
|
let content = fs::read_to_string(path)
|
|
191
194
|
.map_err(|e| GraphError::FileRead {
|
|
@@ -201,10 +204,12 @@ fn load_package(path: &Path) -> Result<Package, GraphError> {
|
|
|
201
204
|
|
|
202
205
|
Ok(Package::from_manifest(manifest))
|
|
203
206
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
"""
|
|
208
|
+
] ;
|
|
209
|
+
cs:do [
|
|
210
|
+
cs:description "Chain errors to preserve the full error trail." ;
|
|
211
|
+
cs:language "rust" ;
|
|
212
|
+
cs:code """
|
|
208
213
|
#[derive(Error, Debug)]
|
|
209
214
|
pub enum AppError {
|
|
210
215
|
#[error("Failed to load configuration")]
|
|
@@ -220,10 +225,12 @@ pub enum AppError {
|
|
|
220
225
|
source: GraphError,
|
|
221
226
|
},
|
|
222
227
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
"""
|
|
229
|
+
] ;
|
|
230
|
+
cs:do [
|
|
231
|
+
cs:description "Include actionable information in error messages." ;
|
|
232
|
+
cs:language "rust" ;
|
|
233
|
+
cs:code """
|
|
227
234
|
#[derive(Error, Debug)]
|
|
228
235
|
pub enum ValidationError {
|
|
229
236
|
#[error("Package name '{name}' is invalid: {reason}. Valid names contain only alphanumeric characters, hyphens, and underscores.")]
|
|
@@ -232,11 +239,12 @@ pub enum ValidationError {
|
|
|
232
239
|
reason: &'static str,
|
|
233
240
|
},
|
|
234
241
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
cs:
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
"""
|
|
243
|
+
] ;
|
|
244
|
+
cs:dont [
|
|
245
|
+
cs:description "Use generic string errors that lose context." ;
|
|
246
|
+
cs:language "rust" ;
|
|
247
|
+
cs:code """
|
|
240
248
|
// Bad: Loses type information and context
|
|
241
249
|
fn load_config(path: &Path) -> Result<Config, String> {
|
|
242
250
|
let content = fs::read_to_string(path)
|
|
@@ -245,10 +253,12 @@ fn load_config(path: &Path) -> Result<Config, String> {
|
|
|
245
253
|
toml::from_str(&content)
|
|
246
254
|
.map_err(|e| e.to_string()) // Which file? What went wrong?
|
|
247
255
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
256
|
+
"""
|
|
257
|
+
] ;
|
|
258
|
+
cs:dont [
|
|
259
|
+
cs:description "Discard error information with unwrap or expect in library code." ;
|
|
260
|
+
cs:language "rust" ;
|
|
261
|
+
cs:code """
|
|
252
262
|
// Bad: Panics instead of propagating
|
|
253
263
|
fn parse_uri(s: &str) -> Uri {
|
|
254
264
|
Uri::parse(s).unwrap() // Crashes on invalid input!
|
|
@@ -256,19 +266,23 @@ fn parse_uri(s: &str) -> Uri {
|
|
|
256
266
|
|
|
257
267
|
// Bad: expect without context
|
|
258
268
|
let file = File::open(path).expect("failed"); // Which path?
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
"""
|
|
270
|
+
] ;
|
|
271
|
+
cs:dont [
|
|
272
|
+
cs:description "Create error types without Display or Debug." ;
|
|
273
|
+
cs:language "rust" ;
|
|
274
|
+
cs:code """
|
|
263
275
|
// Bad: Unusable error type
|
|
264
276
|
pub struct MyError {
|
|
265
277
|
code: i32,
|
|
266
278
|
// No Display impl - can't print meaningful message
|
|
267
279
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
"""
|
|
281
|
+
] ;
|
|
282
|
+
cs:dont [
|
|
283
|
+
cs:description "Use anyhow/eyre in library code (ok for applications)." ;
|
|
284
|
+
cs:language "rust" ;
|
|
285
|
+
cs:code """
|
|
272
286
|
// Bad in libraries: Erases type information
|
|
273
287
|
pub fn process() -> anyhow::Result<()> {
|
|
274
288
|
// Callers can't match on specific error variants
|
|
@@ -278,8 +292,8 @@ pub fn process() -> anyhow::Result<()> {
|
|
|
278
292
|
pub fn process() -> Result<(), ProcessError> {
|
|
279
293
|
// Callers can handle specific cases
|
|
280
294
|
}
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
"""
|
|
296
|
+
] .
|
|
283
297
|
|
|
284
298
|
# =============================================================================
|
|
285
299
|
# Standard 3: Result Chaining (Monadic Composition)
|
|
@@ -288,16 +302,11 @@ pub fn process() -> Result<(), ProcessError> {
|
|
|
288
302
|
cs:ResultChaining a cs:CodeStandard ;
|
|
289
303
|
cs:name "rust/composition/result-chaining" ;
|
|
290
304
|
cs:hasCategory cs:RustCategory ;
|
|
291
|
-
cs:description """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.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
- Idiomaticity: S (idiomatic Rust; ? operator is standard)
|
|
297
|
-
- FP Purity: S (direct monadic composition / Kleisli arrows)""" ;
|
|
298
|
-
cs:dos """
|
|
299
|
-
(Do) Use the ? operator for clean Result propagation.
|
|
300
|
-
```rust
|
|
305
|
+
cs:description """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.""" ;
|
|
306
|
+
cs:do [
|
|
307
|
+
cs:description "Use the ? operator for clean Result propagation." ;
|
|
308
|
+
cs:language "rust" ;
|
|
309
|
+
cs:code """
|
|
301
310
|
fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
|
|
302
311
|
let project_root = find_project_root()
|
|
303
312
|
.ok_or(GraphError::NoProjectRoot)?;
|
|
@@ -318,20 +327,24 @@ fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
|
|
|
318
327
|
|
|
319
328
|
Ok((store, package_graphs))
|
|
320
329
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
"""
|
|
331
|
+
] ;
|
|
332
|
+
cs:do [
|
|
333
|
+
cs:description "Use and_then for dependent computations." ;
|
|
334
|
+
cs:language "rust" ;
|
|
335
|
+
cs:code """
|
|
325
336
|
fn process_user_input(input: &str) -> Result<Output, ProcessError> {
|
|
326
337
|
parse_input(input)
|
|
327
338
|
.and_then(|parsed| validate(parsed))
|
|
328
339
|
.and_then(|valid| transform(valid))
|
|
329
340
|
.and_then(|transformed| save(transformed))
|
|
330
341
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
342
|
+
"""
|
|
343
|
+
] ;
|
|
344
|
+
cs:do [
|
|
345
|
+
cs:description "Use map for infallible transformations within Result." ;
|
|
346
|
+
cs:language "rust" ;
|
|
347
|
+
cs:code """
|
|
335
348
|
fn get_package_names(path: &Path) -> Result<Vec<String>, IoError> {
|
|
336
349
|
fs::read_dir(path)?
|
|
337
350
|
.filter_map(|entry| entry.ok())
|
|
@@ -347,19 +360,23 @@ fn get_config_value(key: &str) -> Result<String, ConfigError> {
|
|
|
347
360
|
.map(|v| v.to_string())
|
|
348
361
|
.ok_or(ConfigError::MissingKey(key.to_string()))
|
|
349
362
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
"""
|
|
364
|
+
] ;
|
|
365
|
+
cs:do [
|
|
366
|
+
cs:description "Use or_else for fallback computations." ;
|
|
367
|
+
cs:language "rust" ;
|
|
368
|
+
cs:code """
|
|
354
369
|
fn find_config() -> Result<Config, ConfigError> {
|
|
355
370
|
load_config_from_env()
|
|
356
371
|
.or_else(|_| load_config_from_file())
|
|
357
372
|
.or_else(|_| Ok(Config::default()))
|
|
358
373
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
374
|
+
"""
|
|
375
|
+
] ;
|
|
376
|
+
cs:do [
|
|
377
|
+
cs:description "Combine multiple Results with the ? operator in sequence." ;
|
|
378
|
+
cs:language "rust" ;
|
|
379
|
+
cs:code """
|
|
363
380
|
fn setup_application() -> Result<App, SetupError> {
|
|
364
381
|
let config = load_config()?;
|
|
365
382
|
let db = connect_database(&config.db_url)?;
|
|
@@ -368,11 +385,12 @@ fn setup_application() -> Result<App, SetupError> {
|
|
|
368
385
|
|
|
369
386
|
Ok(App { config, db, cache, logger })
|
|
370
387
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
cs:
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
"""
|
|
389
|
+
] ;
|
|
390
|
+
cs:dont [
|
|
391
|
+
cs:description "Use nested match expressions for Result handling." ;
|
|
392
|
+
cs:language "rust" ;
|
|
393
|
+
cs:code """
|
|
376
394
|
// Bad: Deeply nested, hard to follow
|
|
377
395
|
fn process(input: &str) -> Result<Output, Error> {
|
|
378
396
|
match parse(input) {
|
|
@@ -390,35 +408,41 @@ fn process(input: &str) -> Result<Output, Error> {
|
|
|
390
408
|
Err(e) => Err(e.into()),
|
|
391
409
|
}
|
|
392
410
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
411
|
+
"""
|
|
412
|
+
] ;
|
|
413
|
+
cs:dont [
|
|
414
|
+
cs:description "Use unwrap() or expect() to bypass error handling." ;
|
|
415
|
+
cs:language "rust" ;
|
|
416
|
+
cs:code """
|
|
397
417
|
// Bad: Panics on error
|
|
398
418
|
fn load_data() -> Data {
|
|
399
419
|
let content = fs::read_to_string("data.json").unwrap();
|
|
400
420
|
serde_json::from_str(&content).unwrap()
|
|
401
421
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
422
|
+
"""
|
|
423
|
+
] ;
|
|
424
|
+
cs:dont [
|
|
425
|
+
cs:description "Ignore errors silently." ;
|
|
426
|
+
cs:language "rust" ;
|
|
427
|
+
cs:code """
|
|
406
428
|
// Bad: Errors are silently dropped
|
|
407
429
|
fn try_save(data: &Data) {
|
|
408
430
|
let _ = fs::write("data.json", serde_json::to_string(data).unwrap_or_default());
|
|
409
431
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
432
|
+
"""
|
|
433
|
+
] ;
|
|
434
|
+
cs:dont [
|
|
435
|
+
cs:description "Convert all errors to strings early in the chain." ;
|
|
436
|
+
cs:language "rust" ;
|
|
437
|
+
cs:code """
|
|
414
438
|
// Bad: Loses error type information
|
|
415
439
|
fn process() -> Result<(), String> {
|
|
416
440
|
let x = step1().map_err(|e| e.to_string())?;
|
|
417
441
|
let y = step2(x).map_err(|e| e.to_string())?; // Can't distinguish errors
|
|
418
442
|
Ok(())
|
|
419
443
|
}
|
|
420
|
-
|
|
421
|
-
|
|
444
|
+
"""
|
|
445
|
+
] .
|
|
422
446
|
|
|
423
447
|
# =============================================================================
|
|
424
448
|
# Standard 4: Iterator Combinator Pipelines
|
|
@@ -427,16 +451,11 @@ fn process() -> Result<(), String> {
|
|
|
427
451
|
cs:CombinatorPipelines a cs:CodeStandard ;
|
|
428
452
|
cs:name "rust/iterators/combinator-pipelines" ;
|
|
429
453
|
cs:hasCategory cs:RustCategory ;
|
|
430
|
-
cs:description """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.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
- Idiomaticity: S (core Rust idiom)
|
|
436
|
-
- FP Purity: S (direct FP; lazy evaluation, composable)""" ;
|
|
437
|
-
cs:dos """
|
|
438
|
-
(Do) Use iterator chains for data transformations.
|
|
439
|
-
```rust
|
|
454
|
+
cs:description """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.""" ;
|
|
455
|
+
cs:do [
|
|
456
|
+
cs:description "Use iterator chains for data transformations." ;
|
|
457
|
+
cs:language "rust" ;
|
|
458
|
+
cs:code """
|
|
440
459
|
fn get_installed_packages() -> Result<Vec<String>, IoError> {
|
|
441
460
|
let packages_path = sem_packages_dir()?;
|
|
442
461
|
|
|
@@ -457,28 +476,34 @@ fn get_installed_packages() -> Result<Vec<String>, IoError> {
|
|
|
457
476
|
packages.sort();
|
|
458
477
|
Ok(packages)
|
|
459
478
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
479
|
+
"""
|
|
480
|
+
] ;
|
|
481
|
+
cs:do [
|
|
482
|
+
cs:description "Use flat_map for one-to-many transformations." ;
|
|
483
|
+
cs:language "rust" ;
|
|
484
|
+
cs:code """
|
|
464
485
|
fn get_all_dependencies(packages: &[Package]) -> Vec<Dependency> {
|
|
465
486
|
packages.iter()
|
|
466
487
|
.flat_map(|pkg| pkg.dependencies())
|
|
467
488
|
.collect()
|
|
468
489
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
490
|
+
"""
|
|
491
|
+
] ;
|
|
492
|
+
cs:do [
|
|
493
|
+
cs:description "Use filter_map to combine filter and map operations." ;
|
|
494
|
+
cs:language "rust" ;
|
|
495
|
+
cs:code """
|
|
473
496
|
fn parse_valid_numbers(strings: &[&str]) -> Vec<i32> {
|
|
474
497
|
strings.iter()
|
|
475
498
|
.filter_map(|s| s.parse::<i32>().ok())
|
|
476
499
|
.collect()
|
|
477
500
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
501
|
+
"""
|
|
502
|
+
] ;
|
|
503
|
+
cs:do [
|
|
504
|
+
cs:description "Use fold/reduce for accumulating results." ;
|
|
505
|
+
cs:language "rust" ;
|
|
506
|
+
cs:code """
|
|
482
507
|
fn total_size(files: &[PathBuf]) -> u64 {
|
|
483
508
|
files.iter()
|
|
484
509
|
.filter_map(|p| fs::metadata(p).ok())
|
|
@@ -490,10 +515,12 @@ fn merge_configs(configs: &[Config]) -> Config {
|
|
|
490
515
|
configs.iter()
|
|
491
516
|
.fold(Config::default(), |acc, cfg| acc.merge(cfg))
|
|
492
517
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
518
|
+
"""
|
|
519
|
+
] ;
|
|
520
|
+
cs:do [
|
|
521
|
+
cs:description "Chain multiple operations for complex transformations." ;
|
|
522
|
+
cs:language "rust" ;
|
|
523
|
+
cs:code """
|
|
497
524
|
fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>> {
|
|
498
525
|
entries.iter()
|
|
499
526
|
.filter(|e| e.level >= LogLevel::Warning)
|
|
@@ -505,10 +532,12 @@ fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>>
|
|
|
505
532
|
acc
|
|
506
533
|
})
|
|
507
534
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
535
|
+
"""
|
|
536
|
+
] ;
|
|
537
|
+
cs:do [
|
|
538
|
+
cs:description "Use collect with turbofish for type-driven collection." ;
|
|
539
|
+
cs:language "rust" ;
|
|
540
|
+
cs:code """
|
|
512
541
|
// Collect into different types based on need
|
|
513
542
|
let vec: Vec<_> = iter.collect();
|
|
514
543
|
let set: HashSet<_> = iter.collect();
|
|
@@ -518,11 +547,12 @@ let map: HashMap<_, _> = iter.map(|x| (x.id, x)).collect();
|
|
|
518
547
|
let results: Result<Vec<_>, _> = items.iter()
|
|
519
548
|
.map(|item| process(item))
|
|
520
549
|
.collect();
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
cs:
|
|
524
|
-
|
|
525
|
-
|
|
550
|
+
"""
|
|
551
|
+
] ;
|
|
552
|
+
cs:dont [
|
|
553
|
+
cs:description "Use imperative loops when combinators are clearer." ;
|
|
554
|
+
cs:language "rust" ;
|
|
555
|
+
cs:code """
|
|
526
556
|
// Bad: Imperative style obscures intent
|
|
527
557
|
fn get_names(users: &[User]) -> Vec<String> {
|
|
528
558
|
let mut names = Vec::new();
|
|
@@ -541,10 +571,12 @@ fn get_names(users: &[User]) -> Vec<String> {
|
|
|
541
571
|
.map(|u| u.name.clone())
|
|
542
572
|
.collect()
|
|
543
573
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
574
|
+
"""
|
|
575
|
+
] ;
|
|
576
|
+
cs:dont [
|
|
577
|
+
cs:description "Collect intermediate results unnecessarily." ;
|
|
578
|
+
cs:language "rust" ;
|
|
579
|
+
cs:code """
|
|
548
580
|
// Bad: Unnecessary allocation
|
|
549
581
|
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
|
550
582
|
let mapped: Vec<_> = filtered.iter().map(|x| x.value).collect();
|
|
@@ -555,10 +587,12 @@ let result: i32 = items.iter()
|
|
|
555
587
|
.filter(|x| x.valid)
|
|
556
588
|
.map(|x| x.value)
|
|
557
589
|
.sum();
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
590
|
+
"""
|
|
591
|
+
] ;
|
|
592
|
+
cs:dont [
|
|
593
|
+
cs:description "Use for loops just to build up a Vec." ;
|
|
594
|
+
cs:language "rust" ;
|
|
595
|
+
cs:code """
|
|
562
596
|
// Bad: Manual Vec building
|
|
563
597
|
let mut results = Vec::new();
|
|
564
598
|
for item in items {
|
|
@@ -567,10 +601,12 @@ for item in items {
|
|
|
567
601
|
|
|
568
602
|
// Good: Use map and collect
|
|
569
603
|
let results: Vec<_> = items.iter().map(transform).collect();
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
604
|
+
"""
|
|
605
|
+
] ;
|
|
606
|
+
cs:dont [
|
|
607
|
+
cs:description "Nest loops when flat_map works." ;
|
|
608
|
+
cs:language "rust" ;
|
|
609
|
+
cs:code """
|
|
574
610
|
// Bad: Nested loops
|
|
575
611
|
let mut all_items = Vec::new();
|
|
576
612
|
for container in containers {
|
|
@@ -583,8 +619,8 @@ for container in containers {
|
|
|
583
619
|
let all_items: Vec<_> = containers.iter()
|
|
584
620
|
.flat_map(|c| c.items())
|
|
585
621
|
.collect();
|
|
586
|
-
|
|
587
|
-
|
|
622
|
+
"""
|
|
623
|
+
] .
|
|
588
624
|
|
|
589
625
|
# =============================================================================
|
|
590
626
|
# Standard 5: Discriminated Unions (Sum Types)
|
|
@@ -593,16 +629,11 @@ let all_items: Vec<_> = containers.iter()
|
|
|
593
629
|
cs:DiscriminatedUnions a cs:CodeStandard ;
|
|
594
630
|
cs:name "rust/types/discriminated-unions" ;
|
|
595
631
|
cs:hasCategory cs:RustCategory ;
|
|
596
|
-
cs:description """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.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
- Idiomaticity: S (Rust's killer feature)
|
|
602
|
-
- FP Purity: S (algebraic data types; Haskell-equivalent)""" ;
|
|
603
|
-
cs:dos """
|
|
604
|
-
(Do) Use enums to model mutually exclusive states.
|
|
605
|
-
```rust
|
|
632
|
+
cs:description """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.""" ;
|
|
633
|
+
cs:do [
|
|
634
|
+
cs:description "Use enums to model mutually exclusive states." ;
|
|
635
|
+
cs:language "rust" ;
|
|
636
|
+
cs:code """
|
|
606
637
|
#[derive(Debug, Clone, PartialEq)]
|
|
607
638
|
pub enum DepSpec {
|
|
608
639
|
Workspace(String),
|
|
@@ -626,10 +657,12 @@ impl DepSpec {
|
|
|
626
657
|
}
|
|
627
658
|
}
|
|
628
659
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
660
|
+
"""
|
|
661
|
+
] ;
|
|
662
|
+
cs:do [
|
|
663
|
+
cs:description "Use enums for state machines with compile-time guarantees." ;
|
|
664
|
+
cs:language "rust" ;
|
|
665
|
+
cs:code """
|
|
633
666
|
enum ConnectionState {
|
|
634
667
|
Disconnected,
|
|
635
668
|
Connecting { attempt: u32 },
|
|
@@ -656,10 +689,12 @@ impl ConnectionState {
|
|
|
656
689
|
}
|
|
657
690
|
}
|
|
658
691
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
692
|
+
"""
|
|
693
|
+
] ;
|
|
694
|
+
cs:do [
|
|
695
|
+
cs:description "Use enums to make invalid states unrepresentable." ;
|
|
696
|
+
cs:language "rust" ;
|
|
697
|
+
cs:code """
|
|
663
698
|
// User can be either anonymous or authenticated, never both
|
|
664
699
|
enum User {
|
|
665
700
|
Anonymous,
|
|
@@ -676,10 +711,12 @@ enum FieldState<T> {
|
|
|
676
711
|
Touched { value: T, errors: Vec<ValidationError> },
|
|
677
712
|
Submitted { value: T },
|
|
678
713
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
714
|
+
"""
|
|
715
|
+
] ;
|
|
716
|
+
cs:do [
|
|
717
|
+
cs:description "Leverage exhaustive matching for safety." ;
|
|
718
|
+
cs:language "rust" ;
|
|
719
|
+
cs:code """
|
|
683
720
|
fn handle_result(result: QueryResult) -> Response {
|
|
684
721
|
match result {
|
|
685
722
|
QueryResult::Success(data) => Response::json(data),
|
|
@@ -691,11 +728,12 @@ fn handle_result(result: QueryResult) -> Response {
|
|
|
691
728
|
// Compiler error if we forget a variant!
|
|
692
729
|
}
|
|
693
730
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
cs:
|
|
697
|
-
|
|
698
|
-
|
|
731
|
+
"""
|
|
732
|
+
] ;
|
|
733
|
+
cs:dont [
|
|
734
|
+
cs:description "Use boolean flags for mutually exclusive states." ;
|
|
735
|
+
cs:language "rust" ;
|
|
736
|
+
cs:code """
|
|
699
737
|
// Bad: Multiple bools can have invalid combinations
|
|
700
738
|
struct User {
|
|
701
739
|
is_anonymous: bool,
|
|
@@ -707,10 +745,12 @@ struct User {
|
|
|
707
745
|
struct Connection {
|
|
708
746
|
state: String, // "connected", "disconnected", typos possible
|
|
709
747
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
748
|
+
"""
|
|
749
|
+
] ;
|
|
750
|
+
cs:dont [
|
|
751
|
+
cs:description "Use Option when you need more than two states." ;
|
|
752
|
+
cs:language "rust" ;
|
|
753
|
+
cs:code """
|
|
714
754
|
// Bad: Option doesn't capture "loading" vs "error" vs "empty"
|
|
715
755
|
struct DataView {
|
|
716
756
|
data: Option<Vec<Item>>, // Is None loading, error, or empty?
|
|
@@ -723,10 +763,12 @@ enum DataState {
|
|
|
723
763
|
Loaded(Vec<Item>),
|
|
724
764
|
Error(String),
|
|
725
765
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
766
|
+
"""
|
|
767
|
+
] ;
|
|
768
|
+
cs:dont [
|
|
769
|
+
cs:description "Use inheritance-like patterns with trait objects when enums suffice." ;
|
|
770
|
+
cs:language "rust" ;
|
|
771
|
+
cs:code """
|
|
730
772
|
// Bad: Runtime dispatch when compile-time would work
|
|
731
773
|
trait Shape {
|
|
732
774
|
fn area(&self) -> f64;
|
|
@@ -748,10 +790,12 @@ impl Shape {
|
|
|
748
790
|
}
|
|
749
791
|
}
|
|
750
792
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
793
|
+
"""
|
|
794
|
+
] ;
|
|
795
|
+
cs:dont [
|
|
796
|
+
cs:description "Use integers or strings as type discriminators." ;
|
|
797
|
+
cs:language "rust" ;
|
|
798
|
+
cs:code """
|
|
755
799
|
// Bad: Magic numbers
|
|
756
800
|
const USER_TYPE_ADMIN: i32 = 1;
|
|
757
801
|
const USER_TYPE_MEMBER: i32 = 2;
|
|
@@ -759,8 +803,8 @@ struct User { user_type: i32 }
|
|
|
759
803
|
|
|
760
804
|
// Bad: Stringly typed
|
|
761
805
|
struct Message { msg_type: String } // "request", "response", typos!
|
|
762
|
-
|
|
763
|
-
|
|
806
|
+
"""
|
|
807
|
+
] .
|
|
764
808
|
|
|
765
809
|
# =============================================================================
|
|
766
810
|
# Standard 6: Kleisli Composition
|
|
@@ -769,16 +813,11 @@ struct Message { msg_type: String } // "request", "response", typos!
|
|
|
769
813
|
cs:KleisliComposition a cs:CodeStandard ;
|
|
770
814
|
cs:name "rust/functions/kleisli-composition" ;
|
|
771
815
|
cs:hasCategory cs:RustCategory ;
|
|
772
|
-
cs:description """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.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
- Idiomaticity: A (natural with ? and and_then)
|
|
778
|
-
- FP Purity: S (direct Kleisli arrow pattern)""" ;
|
|
779
|
-
cs:dos """
|
|
780
|
-
(Do) Design functions as Kleisli arrows: `A -> Result<B, E>`.
|
|
781
|
-
```rust
|
|
816
|
+
cs:description """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.""" ;
|
|
817
|
+
cs:do [
|
|
818
|
+
cs:description "Design functions as Kleisli arrows: `A -> Result<B, E>`." ;
|
|
819
|
+
cs:language "rust" ;
|
|
820
|
+
cs:code """
|
|
782
821
|
// Each function is a Kleisli arrow that can be composed
|
|
783
822
|
fn parse_config(input: &str) -> Result<RawConfig, ParseError> { /* ... */ }
|
|
784
823
|
fn validate_config(raw: RawConfig) -> Result<ValidConfig, ValidationError> { /* ... */ }
|
|
@@ -801,10 +840,12 @@ fn process_input(s: &str) -> Result<Output, ProcessError> {
|
|
|
801
840
|
.and_then(transform)
|
|
802
841
|
.and_then(finalize)
|
|
803
842
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
843
|
+
"""
|
|
844
|
+
] ;
|
|
845
|
+
cs:do [
|
|
846
|
+
cs:description "Use combinators to compose fallible operations." ;
|
|
847
|
+
cs:language "rust" ;
|
|
848
|
+
cs:code """
|
|
808
849
|
impl DepSpec {
|
|
809
850
|
pub fn from_value(value: &DependencyValue) -> Result<Self, ParseError> {
|
|
810
851
|
match value {
|
|
@@ -821,10 +862,12 @@ impl DepSpec {
|
|
|
821
862
|
}
|
|
822
863
|
}
|
|
823
864
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
865
|
+
"""
|
|
866
|
+
] ;
|
|
867
|
+
cs:do [
|
|
868
|
+
cs:description "Create helper traits for method chaining when needed." ;
|
|
869
|
+
cs:language "rust" ;
|
|
870
|
+
cs:code """
|
|
828
871
|
trait ResultExt<T, E> {
|
|
829
872
|
fn and_try<U, F>(self, f: F) -> Result<U, E>
|
|
830
873
|
where
|
|
@@ -845,10 +888,12 @@ let result = input
|
|
|
845
888
|
.and_try(step1)
|
|
846
889
|
.and_try(step2)
|
|
847
890
|
.and_try(step3);
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
891
|
+
"""
|
|
892
|
+
] ;
|
|
893
|
+
cs:do [
|
|
894
|
+
cs:description "Use the pipe pattern for readability." ;
|
|
895
|
+
cs:language "rust" ;
|
|
896
|
+
cs:code """
|
|
852
897
|
// With a pipe trait or tap crate
|
|
853
898
|
trait Pipe: Sized {
|
|
854
899
|
fn pipe<F, R>(self, f: F) -> R where F: FnOnce(Self) -> R {
|
|
@@ -862,11 +907,12 @@ let result = input
|
|
|
862
907
|
.pipe(parse)
|
|
863
908
|
.and_then(|x| x.pipe(validate))
|
|
864
909
|
.and_then(|x| x.pipe(transform));
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
cs:
|
|
868
|
-
|
|
869
|
-
|
|
910
|
+
"""
|
|
911
|
+
] ;
|
|
912
|
+
cs:dont [
|
|
913
|
+
cs:description "Mix side effects into pure transformation chains." ;
|
|
914
|
+
cs:language "rust" ;
|
|
915
|
+
cs:code """
|
|
870
916
|
// Bad: Side effects hidden in chain
|
|
871
917
|
fn process(input: &str) -> Result<Output, Error> {
|
|
872
918
|
parse(input)
|
|
@@ -886,10 +932,12 @@ fn process(input: &str) -> Result<Output, Error> {
|
|
|
886
932
|
let validated = validate(parsed)?;
|
|
887
933
|
transform(validated)
|
|
888
934
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
935
|
+
"""
|
|
936
|
+
] ;
|
|
937
|
+
cs:dont [
|
|
938
|
+
cs:description "Break the chain with early returns when and_then works." ;
|
|
939
|
+
cs:language "rust" ;
|
|
940
|
+
cs:code """
|
|
893
941
|
// Bad: Breaks the monadic flow
|
|
894
942
|
fn process(input: &str) -> Result<Output, Error> {
|
|
895
943
|
let parsed = parse(input)?;
|
|
@@ -911,17 +959,19 @@ fn process(input: &str) -> Result<Output, Error> {
|
|
|
911
959
|
.and_then(|t| (!t.is_empty()).then_some(t).ok_or(Error::Empty))
|
|
912
960
|
.map(finalize)
|
|
913
961
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
962
|
+
"""
|
|
963
|
+
] ;
|
|
964
|
+
cs:dont [
|
|
965
|
+
cs:description "Use unwrap in the middle of a chain." ;
|
|
966
|
+
cs:language "rust" ;
|
|
967
|
+
cs:code """
|
|
918
968
|
// Bad: Panics break the monadic abstraction
|
|
919
969
|
let result = items.iter()
|
|
920
970
|
.map(|x| parse(x).unwrap()) // Panic!
|
|
921
971
|
.filter(|x| x.is_valid())
|
|
922
972
|
.collect();
|
|
923
|
-
|
|
924
|
-
|
|
973
|
+
"""
|
|
974
|
+
] .
|
|
925
975
|
|
|
926
976
|
# =============================================================================
|
|
927
977
|
# Standard 7: Typestate Guards (Parse, Don't Validate)
|
|
@@ -930,16 +980,11 @@ let result = items.iter()
|
|
|
930
980
|
cs:TypestateGuards a cs:CodeStandard ;
|
|
931
981
|
cs:name "rust/validation/typestate-guards" ;
|
|
932
982
|
cs:hasCategory cs:RustCategory ;
|
|
933
|
-
cs:description """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.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
- Idiomaticity: A (recommended Rust pattern)
|
|
939
|
-
- FP Purity: A (Haskell-inspired 'make illegal states unrepresentable')""" ;
|
|
940
|
-
cs:dos """
|
|
941
|
-
(Do) Validate in constructors to ensure type invariants.
|
|
942
|
-
```rust
|
|
983
|
+
cs:description """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.""" ;
|
|
984
|
+
cs:do [
|
|
985
|
+
cs:description "Validate in constructors to ensure type invariants." ;
|
|
986
|
+
cs:language "rust" ;
|
|
987
|
+
cs:code """
|
|
943
988
|
impl Manifest {
|
|
944
989
|
pub fn from_path(path: &Path) -> Result<Self, ManifestError> {
|
|
945
990
|
let content = fs::read_to_string(path)
|
|
@@ -962,10 +1007,12 @@ impl Manifest {
|
|
|
962
1007
|
}
|
|
963
1008
|
}
|
|
964
1009
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1010
|
+
"""
|
|
1011
|
+
] ;
|
|
1012
|
+
cs:do [
|
|
1013
|
+
cs:description "Use the typestate pattern for compile-time state enforcement." ;
|
|
1014
|
+
cs:language "rust" ;
|
|
1015
|
+
cs:code """
|
|
969
1016
|
// States as zero-sized types
|
|
970
1017
|
struct Draft;
|
|
971
1018
|
struct Published;
|
|
@@ -1001,10 +1048,12 @@ impl Article<Published> {
|
|
|
1001
1048
|
// Compile-time enforcement:
|
|
1002
1049
|
// article_draft.view(); // Error: method not found
|
|
1003
1050
|
// article_published.publish(); // Error: method not found
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1051
|
+
"""
|
|
1052
|
+
] ;
|
|
1053
|
+
cs:do [
|
|
1054
|
+
cs:description "Parse into validated types at system boundaries." ;
|
|
1055
|
+
cs:language "rust" ;
|
|
1056
|
+
cs:code """
|
|
1008
1057
|
// Raw input from external source
|
|
1009
1058
|
struct RawUserInput {
|
|
1010
1059
|
email: String,
|
|
@@ -1036,10 +1085,12 @@ impl Email {
|
|
|
1036
1085
|
}
|
|
1037
1086
|
}
|
|
1038
1087
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1088
|
+
"""
|
|
1089
|
+
] ;
|
|
1090
|
+
cs:do [
|
|
1091
|
+
cs:description "Make the validated state obvious in function signatures." ;
|
|
1092
|
+
cs:language "rust" ;
|
|
1093
|
+
cs:code """
|
|
1043
1094
|
// Functions that require validation communicate it via types
|
|
1044
1095
|
fn send_email(to: &Email, subject: &str, body: &str) -> Result<(), SendError> {
|
|
1045
1096
|
// Email is already validated - no need to check again
|
|
@@ -1048,11 +1099,12 @@ fn send_email(to: &Email, subject: &str, body: &str) -> Result<(), SendError> {
|
|
|
1048
1099
|
fn create_account(user: ValidatedUser) -> Result<Account, AccountError> {
|
|
1049
1100
|
// All fields are pre-validated
|
|
1050
1101
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
cs:
|
|
1054
|
-
|
|
1055
|
-
|
|
1102
|
+
"""
|
|
1103
|
+
] ;
|
|
1104
|
+
cs:dont [
|
|
1105
|
+
cs:description "Scatter validation logic throughout the codebase." ;
|
|
1106
|
+
cs:language "rust" ;
|
|
1107
|
+
cs:code """
|
|
1056
1108
|
// Bad: Validation repeated everywhere
|
|
1057
1109
|
fn send_email(to: &str, subject: &str, body: &str) -> Result<(), Error> {
|
|
1058
1110
|
if !to.contains('@') {
|
|
@@ -1067,10 +1119,12 @@ fn save_user(email: &str) -> Result<(), Error> {
|
|
|
1067
1119
|
}
|
|
1068
1120
|
// ... save logic
|
|
1069
1121
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1122
|
+
"""
|
|
1123
|
+
] ;
|
|
1124
|
+
cs:dont [
|
|
1125
|
+
cs:description "Allow construction of invalid objects." ;
|
|
1126
|
+
cs:language "rust" ;
|
|
1127
|
+
cs:code """
|
|
1074
1128
|
// Bad: Public fields allow invalid state
|
|
1075
1129
|
pub struct Email {
|
|
1076
1130
|
pub address: String, // Anyone can set invalid value
|
|
@@ -1082,10 +1136,12 @@ impl User {
|
|
|
1082
1136
|
Self { email, age } // Could be invalid!
|
|
1083
1137
|
}
|
|
1084
1138
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1139
|
+
"""
|
|
1140
|
+
] ;
|
|
1141
|
+
cs:dont [
|
|
1142
|
+
cs:description "Use validation functions that return bool." ;
|
|
1143
|
+
cs:language "rust" ;
|
|
1144
|
+
cs:code """
|
|
1089
1145
|
// Bad: Caller can ignore the result
|
|
1090
1146
|
fn is_valid_email(s: &str) -> bool {
|
|
1091
1147
|
s.contains('@')
|
|
@@ -1099,10 +1155,12 @@ if is_valid_email(&email) {
|
|
|
1099
1155
|
// Good: Parsing forces handling
|
|
1100
1156
|
fn parse_email(s: &str) -> Result<Email, ValidationError>
|
|
1101
1157
|
// Caller must handle the Result
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1158
|
+
"""
|
|
1159
|
+
] ;
|
|
1160
|
+
cs:dont [
|
|
1161
|
+
cs:description "Re-validate already-validated data." ;
|
|
1162
|
+
cs:language "rust" ;
|
|
1163
|
+
cs:code """
|
|
1106
1164
|
// Bad: Redundant validation
|
|
1107
1165
|
fn process(user: ValidatedUser) -> Result<(), Error> {
|
|
1108
1166
|
// ValidatedUser is already valid by construction!
|
|
@@ -1111,8 +1169,8 @@ fn process(user: ValidatedUser) -> Result<(), Error> {
|
|
|
1111
1169
|
}
|
|
1112
1170
|
// ...
|
|
1113
1171
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1172
|
+
"""
|
|
1173
|
+
] .
|
|
1116
1174
|
|
|
1117
1175
|
# =============================================================================
|
|
1118
1176
|
# Standard 8: Small, Composable Traits
|
|
@@ -1121,16 +1179,11 @@ fn process(user: ValidatedUser) -> Result<(), Error> {
|
|
|
1121
1179
|
cs:SmallComposableTraits a cs:CodeStandard ;
|
|
1122
1180
|
cs:name "rust/traits/small-composable" ;
|
|
1123
1181
|
cs:hasCategory cs:RustCategory ;
|
|
1124
|
-
cs:description """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.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
- Idiomaticity: S (Rust's trait system shines here)
|
|
1130
|
-
- FP Purity: A (typeclass-inspired composition)""" ;
|
|
1131
|
-
cs:dos """
|
|
1132
|
-
(Do) Design small, single-purpose traits.
|
|
1133
|
-
```rust
|
|
1182
|
+
cs:description """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.""" ;
|
|
1183
|
+
cs:do [
|
|
1184
|
+
cs:description "Design small, single-purpose traits." ;
|
|
1185
|
+
cs:language "rust" ;
|
|
1186
|
+
cs:code """
|
|
1134
1187
|
/// Can be resolved from a prefixed form to a full URI
|
|
1135
1188
|
trait Resolvable {
|
|
1136
1189
|
fn resolve(&self, prefix_map: &PrefixMap) -> String;
|
|
@@ -1152,10 +1205,12 @@ trait Loadable: Sized {
|
|
|
1152
1205
|
type Error;
|
|
1153
1206
|
fn load(path: &Path) -> Result<Self, Self::Error>;
|
|
1154
1207
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1208
|
+
"""
|
|
1209
|
+
] ;
|
|
1210
|
+
cs:do [
|
|
1211
|
+
cs:description "Compose traits using supertraits and bounds." ;
|
|
1212
|
+
cs:language "rust" ;
|
|
1213
|
+
cs:code """
|
|
1159
1214
|
// Compose small traits into larger capabilities
|
|
1160
1215
|
trait UriHandler: Resolvable + Compactable {}
|
|
1161
1216
|
|
|
@@ -1166,10 +1221,12 @@ impl<T: Resolvable + Compactable> UriHandler for T {}
|
|
|
1166
1221
|
fn process_uri<T: Resolvable + Display>(uri: &T, map: &PrefixMap) -> String {
|
|
1167
1222
|
format!("{} -> {}", uri, uri.resolve(map))
|
|
1168
1223
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1224
|
+
"""
|
|
1225
|
+
] ;
|
|
1226
|
+
cs:do [
|
|
1227
|
+
cs:description "Use extension traits to add methods to existing types." ;
|
|
1228
|
+
cs:language "rust" ;
|
|
1229
|
+
cs:code """
|
|
1173
1230
|
trait ResultExt<T, E> {
|
|
1174
1231
|
fn context(self, msg: &str) -> Result<T, ContextError<E>>;
|
|
1175
1232
|
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, ContextError<E>>;
|
|
@@ -1188,10 +1245,12 @@ impl<T, E> ResultExt<T, E> for Result<T, E> {
|
|
|
1188
1245
|
// Usage
|
|
1189
1246
|
let data = fs::read_to_string(path)
|
|
1190
1247
|
.context("failed to read config")?;
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1248
|
+
"""
|
|
1249
|
+
] ;
|
|
1250
|
+
cs:do [
|
|
1251
|
+
cs:description "Implement standard library traits for interoperability." ;
|
|
1252
|
+
cs:language "rust" ;
|
|
1253
|
+
cs:code """
|
|
1195
1254
|
impl Default for SemStore {
|
|
1196
1255
|
fn default() -> Self {
|
|
1197
1256
|
Self::new().expect("Failed to create default SemStore")
|
|
@@ -1211,11 +1270,12 @@ impl FromStr for Version {
|
|
|
1211
1270
|
// parsing logic
|
|
1212
1271
|
}
|
|
1213
1272
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
cs:
|
|
1217
|
-
|
|
1218
|
-
|
|
1273
|
+
"""
|
|
1274
|
+
] ;
|
|
1275
|
+
cs:dont [
|
|
1276
|
+
cs:description "Create large, monolithic traits." ;
|
|
1277
|
+
cs:language "rust" ;
|
|
1278
|
+
cs:code """
|
|
1219
1279
|
// Bad: Too many responsibilities
|
|
1220
1280
|
trait Repository {
|
|
1221
1281
|
fn connect(&mut self) -> Result<(), Error>;
|
|
@@ -1230,10 +1290,12 @@ trait Repository {
|
|
|
1230
1290
|
fn execute_query(&self, query: &str) -> Result<QueryResult, Error>;
|
|
1231
1291
|
// ... 20 more methods
|
|
1232
1292
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1293
|
+
"""
|
|
1294
|
+
] ;
|
|
1295
|
+
cs:dont [
|
|
1296
|
+
cs:description "Use trait objects when generics suffice." ;
|
|
1297
|
+
cs:language "rust" ;
|
|
1298
|
+
cs:code """
|
|
1237
1299
|
// Bad: Unnecessary dynamic dispatch
|
|
1238
1300
|
fn process(items: &[Box<dyn Processable>]) {
|
|
1239
1301
|
for item in items {
|
|
@@ -1247,10 +1309,12 @@ fn process<T: Processable>(items: &[T]) {
|
|
|
1247
1309
|
item.process();
|
|
1248
1310
|
}
|
|
1249
1311
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1312
|
+
"""
|
|
1313
|
+
] ;
|
|
1314
|
+
cs:dont [
|
|
1315
|
+
cs:description "Require unused trait methods via blanket requirements." ;
|
|
1316
|
+
cs:language "rust" ;
|
|
1317
|
+
cs:code """
|
|
1254
1318
|
// Bad: Forces implementers to provide unused methods
|
|
1255
1319
|
trait DataStore: Connect + Query + Mutate + Transaction + Cache + Log {
|
|
1256
1320
|
// Most implementers don't need all of these
|
|
@@ -1260,10 +1324,12 @@ trait DataStore: Connect + Query + Mutate + Transaction + Cache + Log {
|
|
|
1260
1324
|
fn process<T: Query + Mutate>(store: &mut T) {
|
|
1261
1325
|
// Only requires the capabilities actually used
|
|
1262
1326
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1327
|
+
"""
|
|
1328
|
+
] ;
|
|
1329
|
+
cs:dont [
|
|
1330
|
+
cs:description "Use associated types when generic parameters work better." ;
|
|
1331
|
+
cs:language "rust" ;
|
|
1332
|
+
cs:code """
|
|
1267
1333
|
// Bad: Can only have one implementation per type
|
|
1268
1334
|
trait Container {
|
|
1269
1335
|
type Item;
|
|
@@ -1275,8 +1341,8 @@ trait Container<T> {
|
|
|
1275
1341
|
fn get(&self) -> &T;
|
|
1276
1342
|
}
|
|
1277
1343
|
// Now Vec<i32> can be Container<i32> AND Container<String> if needed
|
|
1278
|
-
|
|
1279
|
-
|
|
1344
|
+
"""
|
|
1345
|
+
] .
|
|
1280
1346
|
|
|
1281
1347
|
# =============================================================================
|
|
1282
1348
|
# Standard 9: Explicit Absence (Option Handling)
|
|
@@ -1285,16 +1351,11 @@ trait Container<T> {
|
|
|
1285
1351
|
cs:ExplicitAbsence a cs:CodeStandard ;
|
|
1286
1352
|
cs:name "rust/option/explicit-absence" ;
|
|
1287
1353
|
cs:hasCategory cs:RustCategory ;
|
|
1288
|
-
cs:description """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.
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
- Idiomaticity: S (fundamental Rust pattern)
|
|
1294
|
-
- FP Purity: S (Maybe monad equivalent)""" ;
|
|
1295
|
-
cs:dos """
|
|
1296
|
-
(Do) Use Option combinators for clean transformations.
|
|
1297
|
-
```rust
|
|
1354
|
+
cs:description """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.""" ;
|
|
1355
|
+
cs:do [
|
|
1356
|
+
cs:description "Use Option combinators for clean transformations." ;
|
|
1357
|
+
cs:language "rust" ;
|
|
1358
|
+
cs:code """
|
|
1298
1359
|
impl Manifest {
|
|
1299
1360
|
pub fn display_name(&self) -> Option<String> {
|
|
1300
1361
|
self.workspace.as_ref()
|
|
@@ -1310,10 +1371,12 @@ impl Manifest {
|
|
|
1310
1371
|
self.workspace.as_ref().map(|w| w.members.clone())
|
|
1311
1372
|
}
|
|
1312
1373
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1374
|
+
"""
|
|
1375
|
+
] ;
|
|
1376
|
+
cs:do [
|
|
1377
|
+
cs:description "Use unwrap_or and unwrap_or_else for defaults." ;
|
|
1378
|
+
cs:language "rust" ;
|
|
1379
|
+
cs:code """
|
|
1317
1380
|
fn get_config_value(key: &str) -> String {
|
|
1318
1381
|
config.get(key)
|
|
1319
1382
|
.cloned()
|
|
@@ -1326,10 +1389,12 @@ fn get_timeout() -> Duration {
|
|
|
1326
1389
|
.and_then(|s| s.parse().ok())
|
|
1327
1390
|
.unwrap_or(Duration::from_secs(30))
|
|
1328
1391
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1392
|
+
"""
|
|
1393
|
+
] ;
|
|
1394
|
+
cs:do [
|
|
1395
|
+
cs:description "Use ok_or to convert Option to Result." ;
|
|
1396
|
+
cs:language "rust" ;
|
|
1397
|
+
cs:code """
|
|
1333
1398
|
fn find_package(name: &str) -> Result<&Package, PackageError> {
|
|
1334
1399
|
packages.get(name)
|
|
1335
1400
|
.ok_or_else(|| PackageError::NotFound(name.to_string()))
|
|
@@ -1339,10 +1404,12 @@ fn get_required_field<'a>(map: &'a HashMap<String, Value>, key: &str) -> Result<
|
|
|
1339
1404
|
map.get(key)
|
|
1340
1405
|
.ok_or(ConfigError::MissingField(key.to_string()))
|
|
1341
1406
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1407
|
+
"""
|
|
1408
|
+
] ;
|
|
1409
|
+
cs:do [
|
|
1410
|
+
cs:description "Use filter and filter_map for conditional processing." ;
|
|
1411
|
+
cs:language "rust" ;
|
|
1412
|
+
cs:code """
|
|
1346
1413
|
fn find_active_user(id: UserId) -> Option<User> {
|
|
1347
1414
|
users.get(&id).filter(|u| u.is_active)
|
|
1348
1415
|
}
|
|
@@ -1352,20 +1419,23 @@ fn get_valid_entries(entries: &[Entry]) -> Vec<&Entry> {
|
|
|
1352
1419
|
.filter(|e| e.is_valid())
|
|
1353
1420
|
.collect()
|
|
1354
1421
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1422
|
+
"""
|
|
1423
|
+
] ;
|
|
1424
|
+
cs:do [
|
|
1425
|
+
cs:description "Use the ? operator with Option in functions returning Option." ;
|
|
1426
|
+
cs:language "rust" ;
|
|
1427
|
+
cs:code """
|
|
1359
1428
|
fn get_nested_value(data: &Data) -> Option<&str> {
|
|
1360
1429
|
let section = data.sections.get("main")?;
|
|
1361
1430
|
let item = section.items.first()?;
|
|
1362
1431
|
item.value.as_deref()
|
|
1363
1432
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
cs:
|
|
1367
|
-
|
|
1368
|
-
|
|
1433
|
+
"""
|
|
1434
|
+
] ;
|
|
1435
|
+
cs:dont [
|
|
1436
|
+
cs:description "Use sentinel values instead of Option." ;
|
|
1437
|
+
cs:language "rust" ;
|
|
1438
|
+
cs:code """
|
|
1369
1439
|
// Bad: Magic values
|
|
1370
1440
|
fn find_index(items: &[Item], target: &Item) -> i32 {
|
|
1371
1441
|
// Returns -1 if not found - caller might forget to check!
|
|
@@ -1376,10 +1446,12 @@ fn find_index(items: &[Item], target: &Item) -> i32 {
|
|
|
1376
1446
|
fn find_index(items: &[Item], target: &Item) -> Option<usize> {
|
|
1377
1447
|
items.iter().position(|i| i == target)
|
|
1378
1448
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1449
|
+
"""
|
|
1450
|
+
] ;
|
|
1451
|
+
cs:dont [
|
|
1452
|
+
cs:description "Overuse unwrap() or expect() outside of tests." ;
|
|
1453
|
+
cs:language "rust" ;
|
|
1454
|
+
cs:code """
|
|
1383
1455
|
// Bad: Panics on None
|
|
1384
1456
|
let user = users.get(id).unwrap();
|
|
1385
1457
|
let name = user.name.as_ref().unwrap();
|
|
@@ -1387,10 +1459,12 @@ let name = user.name.as_ref().unwrap();
|
|
|
1387
1459
|
// Good: Handle absence explicitly
|
|
1388
1460
|
let user = users.get(id).ok_or(UserError::NotFound(id))?;
|
|
1389
1461
|
let name = user.name.as_deref().unwrap_or("Anonymous");
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1462
|
+
"""
|
|
1463
|
+
] ;
|
|
1464
|
+
cs:dont [
|
|
1465
|
+
cs:description "Check is_some/is_none then unwrap." ;
|
|
1466
|
+
cs:language "rust" ;
|
|
1467
|
+
cs:code """
|
|
1394
1468
|
// Bad: Redundant check
|
|
1395
1469
|
if value.is_some() {
|
|
1396
1470
|
let v = value.unwrap(); // We just checked!
|
|
@@ -1404,10 +1478,12 @@ if let Some(v) = value {
|
|
|
1404
1478
|
|
|
1405
1479
|
// Or even better for side effects
|
|
1406
1480
|
value.map(process);
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1481
|
+
"""
|
|
1482
|
+
] ;
|
|
1483
|
+
cs:dont [
|
|
1484
|
+
cs:description "Create deeply nested Option chains without combinators." ;
|
|
1485
|
+
cs:language "rust" ;
|
|
1486
|
+
cs:code """
|
|
1411
1487
|
// Bad: Nested matching
|
|
1412
1488
|
match outer {
|
|
1413
1489
|
Some(o) => match o.inner {
|
|
@@ -1425,8 +1501,8 @@ outer
|
|
|
1425
1501
|
.and_then(|o| o.inner)
|
|
1426
1502
|
.and_then(|i| i.value)
|
|
1427
1503
|
.map(transform)
|
|
1428
|
-
|
|
1429
|
-
|
|
1504
|
+
"""
|
|
1505
|
+
] .
|
|
1430
1506
|
|
|
1431
1507
|
# =============================================================================
|
|
1432
1508
|
# Standard 10: Property-Based Testing
|
|
@@ -1435,16 +1511,11 @@ outer
|
|
|
1435
1511
|
cs:PropertyBasedTesting a cs:CodeStandard ;
|
|
1436
1512
|
cs:name "rust/testing/property-based" ;
|
|
1437
1513
|
cs:hasCategory cs:RustCategory ;
|
|
1438
|
-
cs:description """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.
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
- Idiomaticity: A (growing Rust practice)
|
|
1444
|
-
- FP Purity: S (QuickCheck heritage; declarative testing)""" ;
|
|
1445
|
-
cs:dos """
|
|
1446
|
-
(Do) Use proptest for property-based testing.
|
|
1447
|
-
```rust
|
|
1514
|
+
cs:description """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.""" ;
|
|
1515
|
+
cs:do [
|
|
1516
|
+
cs:description "Use proptest for property-based testing." ;
|
|
1517
|
+
cs:language "rust" ;
|
|
1518
|
+
cs:code """
|
|
1448
1519
|
use proptest::prelude::*;
|
|
1449
1520
|
|
|
1450
1521
|
proptest! {
|
|
@@ -1467,10 +1538,12 @@ proptest! {
|
|
|
1467
1538
|
}
|
|
1468
1539
|
}
|
|
1469
1540
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1541
|
+
"""
|
|
1542
|
+
] ;
|
|
1543
|
+
cs:do [
|
|
1544
|
+
cs:description "Test algebraic properties (identity, associativity, commutativity)." ;
|
|
1545
|
+
cs:language "rust" ;
|
|
1546
|
+
cs:code """
|
|
1474
1547
|
proptest! {
|
|
1475
1548
|
#[test]
|
|
1476
1549
|
fn merge_associative(a: Config, b: Config, c: Config) {
|
|
@@ -1487,10 +1560,12 @@ proptest! {
|
|
|
1487
1560
|
assert_eq!(merged, config);
|
|
1488
1561
|
}
|
|
1489
1562
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1563
|
+
"""
|
|
1564
|
+
] ;
|
|
1565
|
+
cs:do [
|
|
1566
|
+
cs:description "Test invariants that should hold for all valid inputs." ;
|
|
1567
|
+
cs:language "rust" ;
|
|
1568
|
+
cs:code """
|
|
1494
1569
|
proptest! {
|
|
1495
1570
|
#[test]
|
|
1496
1571
|
fn validated_email_contains_at(s in ".+@.+\\..+") {
|
|
@@ -1511,10 +1586,12 @@ proptest! {
|
|
|
1511
1586
|
}
|
|
1512
1587
|
}
|
|
1513
1588
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1589
|
+
"""
|
|
1590
|
+
] ;
|
|
1591
|
+
cs:do [
|
|
1592
|
+
cs:description "Use custom strategies for domain-specific types." ;
|
|
1593
|
+
cs:language "rust" ;
|
|
1594
|
+
cs:code """
|
|
1518
1595
|
fn valid_package_name() -> impl Strategy<Value = String> {
|
|
1519
1596
|
"[a-z][a-z0-9-]{0,62}[a-z0-9]?"
|
|
1520
1597
|
.prop_filter("Must not have consecutive hyphens", |s| !s.contains("--"))
|
|
@@ -1531,11 +1608,12 @@ proptest! {
|
|
|
1531
1608
|
assert!(PackageName::new(&name).is_ok());
|
|
1532
1609
|
}
|
|
1533
1610
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
cs:
|
|
1537
|
-
|
|
1538
|
-
|
|
1611
|
+
"""
|
|
1612
|
+
] ;
|
|
1613
|
+
cs:dont [
|
|
1614
|
+
cs:description "Only write example-based unit tests for complex logic." ;
|
|
1615
|
+
cs:language "rust" ;
|
|
1616
|
+
cs:code """
|
|
1539
1617
|
// Incomplete: Only tests a few examples
|
|
1540
1618
|
#[test]
|
|
1541
1619
|
fn test_merge() {
|
|
@@ -1545,10 +1623,12 @@ fn test_merge() {
|
|
|
1545
1623
|
assert_eq!(merged.timeout, 20);
|
|
1546
1624
|
// What about edge cases? Overflow? Default values?
|
|
1547
1625
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1626
|
+
"""
|
|
1627
|
+
] ;
|
|
1628
|
+
cs:dont [
|
|
1629
|
+
cs:description "Ignore test failures without understanding the counterexample." ;
|
|
1630
|
+
cs:language "rust" ;
|
|
1631
|
+
cs:code """
|
|
1552
1632
|
// Bad: Ignoring failures
|
|
1553
1633
|
proptest! {
|
|
1554
1634
|
#[test]
|
|
@@ -1557,10 +1637,12 @@ proptest! {
|
|
|
1557
1637
|
// ...
|
|
1558
1638
|
}
|
|
1559
1639
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1640
|
+
"""
|
|
1641
|
+
] ;
|
|
1642
|
+
cs:dont [
|
|
1643
|
+
cs:description "Write properties that are too weak or tautological." ;
|
|
1644
|
+
cs:language "rust" ;
|
|
1645
|
+
cs:code """
|
|
1564
1646
|
// Bad: This always passes, tests nothing useful
|
|
1565
1647
|
proptest! {
|
|
1566
1648
|
#[test]
|
|
@@ -1577,10 +1659,12 @@ proptest! {
|
|
|
1577
1659
|
assert_eq!(add(a, b), a + b); // Just restates the implementation
|
|
1578
1660
|
}
|
|
1579
1661
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1662
|
+
"""
|
|
1663
|
+
] ;
|
|
1664
|
+
cs:dont [
|
|
1665
|
+
cs:description "Generate invalid inputs without proper filtering." ;
|
|
1666
|
+
cs:language "rust" ;
|
|
1667
|
+
cs:code """
|
|
1584
1668
|
// Bad: Generates invalid UTF-8 and panics
|
|
1585
1669
|
proptest! {
|
|
1586
1670
|
#[test]
|
|
@@ -1596,8 +1680,8 @@ proptest! {
|
|
|
1596
1680
|
// ...
|
|
1597
1681
|
}
|
|
1598
1682
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1683
|
+
"""
|
|
1684
|
+
] .
|
|
1601
1685
|
|
|
1602
1686
|
# =============================================================================
|
|
1603
1687
|
# Standard 11: TDD Red-Green-Refactor
|
|
@@ -1606,16 +1690,11 @@ proptest! {
|
|
|
1606
1690
|
cs:TDDRedGreenRefactor a cs:CodeStandard ;
|
|
1607
1691
|
cs:name "rust/testing/tdd-red-green-refactor" ;
|
|
1608
1692
|
cs:hasCategory cs:RustCategory ;
|
|
1609
|
-
cs:description """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.
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
- Idiomaticity: A (industry best practice)
|
|
1615
|
-
- FP Purity: B (supports referential transparency goals)""" ;
|
|
1616
|
-
cs:dos """
|
|
1617
|
-
(Do) Write the test first, watch it fail.
|
|
1618
|
-
```rust
|
|
1693
|
+
cs:description """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.""" ;
|
|
1694
|
+
cs:do [
|
|
1695
|
+
cs:description "Write the test first, watch it fail." ;
|
|
1696
|
+
cs:language "rust" ;
|
|
1697
|
+
cs:code """
|
|
1619
1698
|
// Step 1: RED - Write failing test
|
|
1620
1699
|
#[cfg(test)]
|
|
1621
1700
|
mod tests {
|
|
@@ -1631,10 +1710,12 @@ mod tests {
|
|
|
1631
1710
|
}
|
|
1632
1711
|
|
|
1633
1712
|
// At this point: cargo test fails - Version doesn't exist yet!
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1713
|
+
"""
|
|
1714
|
+
] ;
|
|
1715
|
+
cs:do [
|
|
1716
|
+
cs:description "Implement the minimum code to pass." ;
|
|
1717
|
+
cs:language "rust" ;
|
|
1718
|
+
cs:code """
|
|
1638
1719
|
// Step 2: GREEN - Minimal implementation
|
|
1639
1720
|
pub struct Version {
|
|
1640
1721
|
pub major: u32,
|
|
@@ -1657,10 +1738,12 @@ impl Version {
|
|
|
1657
1738
|
}
|
|
1658
1739
|
|
|
1659
1740
|
// cargo test passes!
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1741
|
+
"""
|
|
1742
|
+
] ;
|
|
1743
|
+
cs:do [
|
|
1744
|
+
cs:description "Refactor while keeping tests green." ;
|
|
1745
|
+
cs:language "rust" ;
|
|
1746
|
+
cs:code """
|
|
1664
1747
|
// Step 3: REFACTOR - Improve design
|
|
1665
1748
|
impl Version {
|
|
1666
1749
|
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
|
@@ -1685,10 +1768,12 @@ impl Version {
|
|
|
1685
1768
|
}
|
|
1686
1769
|
|
|
1687
1770
|
// Tests still pass after refactoring!
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1771
|
+
"""
|
|
1772
|
+
] ;
|
|
1773
|
+
cs:do [
|
|
1774
|
+
cs:description "Add tests for edge cases incrementally." ;
|
|
1775
|
+
cs:language "rust" ;
|
|
1776
|
+
cs:code """
|
|
1692
1777
|
#[test]
|
|
1693
1778
|
fn parse_rejects_empty_string() {
|
|
1694
1779
|
assert!(Version::parse("").is_err());
|
|
@@ -1709,10 +1794,12 @@ fn parse_handles_leading_zeros() {
|
|
|
1709
1794
|
let v = Version::parse("01.02.03").unwrap();
|
|
1710
1795
|
assert_eq!(v.major, 1); // Decide: allow or reject?
|
|
1711
1796
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1797
|
+
"""
|
|
1798
|
+
] ;
|
|
1799
|
+
cs:do [
|
|
1800
|
+
cs:description "Use test modules colocated with implementation." ;
|
|
1801
|
+
cs:language "rust" ;
|
|
1802
|
+
cs:code """
|
|
1716
1803
|
// src/version.rs
|
|
1717
1804
|
pub struct Version { /* ... */ }
|
|
1718
1805
|
|
|
@@ -1728,21 +1815,24 @@ mod tests {
|
|
|
1728
1815
|
#[test]
|
|
1729
1816
|
fn test_parse() { /* ... */ }
|
|
1730
1817
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
cs:
|
|
1734
|
-
|
|
1735
|
-
|
|
1818
|
+
"""
|
|
1819
|
+
] ;
|
|
1820
|
+
cs:dont [
|
|
1821
|
+
cs:description "Write implementation before tests." ;
|
|
1822
|
+
cs:language "rust" ;
|
|
1823
|
+
cs:code """
|
|
1736
1824
|
// Bad: Implementation without tests
|
|
1737
1825
|
pub fn complex_algorithm(input: &str) -> Result<Output, Error> {
|
|
1738
1826
|
// 200 lines of complex logic
|
|
1739
1827
|
// No tests to verify correctness
|
|
1740
1828
|
// No tests to prevent regression
|
|
1741
1829
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1830
|
+
"""
|
|
1831
|
+
] ;
|
|
1832
|
+
cs:dont [
|
|
1833
|
+
cs:description "Write tests that pass trivially or test nothing." ;
|
|
1834
|
+
cs:language "rust" ;
|
|
1835
|
+
cs:code """
|
|
1746
1836
|
// Bad: Test that always passes
|
|
1747
1837
|
#[test]
|
|
1748
1838
|
fn useless_test() {
|
|
@@ -1754,10 +1844,12 @@ fn useless_test() {
|
|
|
1754
1844
|
fn test_without_assertions() {
|
|
1755
1845
|
let _ = Version::parse("1.0.0"); // No assertions!
|
|
1756
1846
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1847
|
+
"""
|
|
1848
|
+
] ;
|
|
1849
|
+
cs:dont [
|
|
1850
|
+
cs:description "Skip the refactor step." ;
|
|
1851
|
+
cs:language "rust" ;
|
|
1852
|
+
cs:code """
|
|
1761
1853
|
// Bad: "It works, ship it!"
|
|
1762
1854
|
impl Version {
|
|
1763
1855
|
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
|
@@ -1772,20 +1864,24 @@ impl Version {
|
|
|
1772
1864
|
Ok(Self { major: a.unwrap(), minor: b.unwrap(), patch: c.unwrap() })
|
|
1773
1865
|
}
|
|
1774
1866
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1867
|
+
"""
|
|
1868
|
+
] ;
|
|
1869
|
+
cs:dont [
|
|
1870
|
+
cs:description "Write tests after the fact that just confirm current behavior." ;
|
|
1871
|
+
cs:language "rust" ;
|
|
1872
|
+
cs:code """
|
|
1779
1873
|
// Bad: "Characterization tests" without understanding intent
|
|
1780
1874
|
#[test]
|
|
1781
1875
|
fn test_weird_behavior() {
|
|
1782
1876
|
// I don't know why it returns 42, but it does, so test it
|
|
1783
1877
|
assert_eq!(mysterious_function("input"), 42);
|
|
1784
1878
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1879
|
+
"""
|
|
1880
|
+
] ;
|
|
1881
|
+
cs:dont [
|
|
1882
|
+
cs:description "Test private implementation details." ;
|
|
1883
|
+
cs:language "rust" ;
|
|
1884
|
+
cs:code """
|
|
1789
1885
|
// Bad: Testing internals that may change
|
|
1790
1886
|
#[test]
|
|
1791
1887
|
fn test_internal_cache_structure() {
|
|
@@ -1802,5 +1898,5 @@ fn test_caching_behavior() {
|
|
|
1802
1898
|
let result2 = obj.expensive_operation(); // Should be cached
|
|
1803
1899
|
assert_eq!(result1, result2);
|
|
1804
1900
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1901
|
+
"""
|
|
1902
|
+
] .
|