depguard-types 0.1.0

Stable depguard protocol types, IDs, and explanation registry
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
//! Explain registry for checks and codes.
//!
//! Maps check IDs and codes to human-readable explanations with remediation guidance.

use crate::ids;

/// Explanation entry for a check or code.
#[derive(Debug, Clone)]
pub struct Explanation {
    /// Short description of the check/code.
    pub title: &'static str,
    /// What the check does and why it exists.
    pub description: &'static str,
    /// How to fix violations.
    pub remediation: &'static str,
    /// Before/after code examples.
    pub examples: ExamplePair,
}

/// Before and after code examples.
#[derive(Debug, Clone)]
pub struct ExamplePair {
    /// Code that would trigger a finding.
    pub before: &'static str,
    /// Code that passes the check.
    pub after: &'static str,
}

/// Look up an explanation by check_id or code.
///
/// Returns `None` if the identifier is not recognized.
pub fn lookup_explanation(identifier: &str) -> Option<Explanation> {
    // Try check_id first, then code
    match identifier {
        // Check IDs
        ids::CHECK_DEPS_NO_WILDCARDS => Some(explain_no_wildcards()),
        ids::CHECK_DEPS_PATH_REQUIRES_VERSION => Some(explain_path_requires_version()),
        ids::CHECK_DEPS_PATH_SAFETY => Some(explain_path_safety()),
        ids::CHECK_DEPS_WORKSPACE_INHERITANCE => Some(explain_workspace_inheritance()),
        ids::CHECK_DEPS_GIT_REQUIRES_VERSION => Some(explain_git_requires_version()),
        ids::CHECK_DEPS_DEV_ONLY_IN_NORMAL => Some(explain_dev_only_in_normal()),
        ids::CHECK_DEPS_DEFAULT_FEATURES_EXPLICIT => Some(explain_default_features_explicit()),
        ids::CHECK_DEPS_NO_MULTIPLE_VERSIONS => Some(explain_no_multiple_versions()),
        ids::CHECK_DEPS_OPTIONAL_UNUSED => Some(explain_optional_unused()),
        ids::CHECK_DEPS_YANKED_VERSIONS => Some(explain_yanked_versions()),
        ids::CHECK_TOOL_RUNTIME => Some(explain_tool_runtime()),

        // Codes
        ids::CODE_WILDCARD_VERSION => Some(explain_wildcard_version()),
        ids::CODE_PATH_WITHOUT_VERSION => Some(explain_path_without_version()),
        ids::CODE_ABSOLUTE_PATH => Some(explain_absolute_path()),
        ids::CODE_PARENT_ESCAPE => Some(explain_parent_escape()),
        ids::CODE_MISSING_WORKSPACE_TRUE => Some(explain_missing_workspace_true()),
        ids::CODE_GIT_WITHOUT_VERSION => Some(explain_git_without_version()),
        ids::CODE_DEV_DEP_IN_NORMAL => Some(explain_dev_dep_in_normal()),
        ids::CODE_DEFAULT_FEATURES_IMPLICIT => Some(explain_default_features_implicit()),
        ids::CODE_DUPLICATE_DIFFERENT_VERSIONS => Some(explain_duplicate_different_versions()),
        ids::CODE_OPTIONAL_NOT_IN_FEATURES => Some(explain_optional_not_in_features()),
        ids::CODE_VERSION_YANKED => Some(explain_version_yanked()),
        ids::CODE_RUNTIME_ERROR => Some(explain_runtime_error()),

        _ => None,
    }
}

/// List all known check IDs.
pub fn all_check_ids() -> &'static [&'static str] {
    &[
        ids::CHECK_DEPS_NO_WILDCARDS,
        ids::CHECK_DEPS_PATH_REQUIRES_VERSION,
        ids::CHECK_DEPS_PATH_SAFETY,
        ids::CHECK_DEPS_WORKSPACE_INHERITANCE,
        ids::CHECK_DEPS_GIT_REQUIRES_VERSION,
        ids::CHECK_DEPS_DEV_ONLY_IN_NORMAL,
        ids::CHECK_DEPS_DEFAULT_FEATURES_EXPLICIT,
        ids::CHECK_DEPS_NO_MULTIPLE_VERSIONS,
        ids::CHECK_DEPS_OPTIONAL_UNUSED,
        ids::CHECK_DEPS_YANKED_VERSIONS,
        ids::CHECK_TOOL_RUNTIME,
    ]
}

/// List all known codes.
pub fn all_codes() -> &'static [&'static str] {
    &[
        ids::CODE_WILDCARD_VERSION,
        ids::CODE_PATH_WITHOUT_VERSION,
        ids::CODE_ABSOLUTE_PATH,
        ids::CODE_PARENT_ESCAPE,
        ids::CODE_MISSING_WORKSPACE_TRUE,
        ids::CODE_GIT_WITHOUT_VERSION,
        ids::CODE_DEV_DEP_IN_NORMAL,
        ids::CODE_DEFAULT_FEATURES_IMPLICIT,
        ids::CODE_DUPLICATE_DIFFERENT_VERSIONS,
        ids::CODE_OPTIONAL_NOT_IN_FEATURES,
        ids::CODE_VERSION_YANKED,
        ids::CODE_RUNTIME_ERROR,
    ]
}

// --- Check-level explanations ---

fn explain_no_wildcards() -> Explanation {
    Explanation {
        title: "No Wildcard Versions",
        description: "\
Detects dependencies declared with wildcard version requirements like `*` or `1.*`.

Wildcard versions are problematic because:
- They allow any version to be selected, including breaking changes
- Builds are not reproducible across different points in time
- Security vulnerabilities in newer versions may be pulled in unknowingly
- cargo publish rejects crates with wildcard dependencies",
        remediation: "\
Replace wildcard versions with explicit semver requirements:
- Use `^1.2.3` (caret, default) for compatible updates within the same major version
- Use `~1.2.3` (tilde) for patch-level updates only
- Use `=1.2.3` for an exact version pin
- Use `>=1.2.0, <2.0.0` for explicit version ranges",
        examples: ExamplePair {
            before: r#"[dependencies]
serde = "*"
tokio = "1.*""#,
            after: r#"[dependencies]
serde = "1.0"
tokio = "1.35""#,
        },
    }
}

fn explain_path_requires_version() -> Explanation {
    Explanation {
        title: "Path Dependencies Require Version",
        description: "\
Detects path dependencies in publishable crates that lack an explicit version.

When publishing a crate to crates.io, Cargo ignores the `path` key and uses only
the version from the registry. If no version is specified:
- The crate cannot be published (cargo publish will fail)
- Users who depend on your crate won't be able to build it

This check only applies to crates that can be published (publish != false).",
        remediation: "\
Add an explicit version alongside the path:

    my-crate = { path = \"../my-crate\", version = \"0.1.0\" }

Alternatively, use workspace inheritance:

    my-crate.workspace = true

Or mark the crate as unpublishable in its Cargo.toml:

    [package]
    publish = false",
        examples: ExamplePair {
            before: r#"[dependencies]
my-lib = { path = "../my-lib" }"#,
            after: r#"[dependencies]
my-lib = { path = "../my-lib", version = "0.1.0" }

# Or use workspace inheritance:
my-lib.workspace = true"#,
        },
    }
}

fn explain_path_safety() -> Explanation {
    Explanation {
        title: "Path Dependency Safety",
        description: "\
Detects path dependencies that use absolute paths or escape the repository root.

This check flags two issues:
1. Absolute paths (e.g., `/home/user/code/lib` or `C:\\Code\\lib`)
2. Parent references (`..`) that escape outside the repository root

Both patterns cause problems:
- Absolute paths are machine-specific and not portable
- Escaping the repo root means the dependency is not version-controlled with the project
- CI/CD builds will fail when paths don't exist on the build machine
- Other contributors cannot build the project without identical directory layouts",
        remediation: "\
Use repo-relative paths that stay within the repository:

    my-crate = { path = \"../sibling-crate\" }  # OK if still in repo
    my-crate = { path = \"crates/my-crate\" }   # Always OK

If you need an external dependency:
- Publish it to crates.io or a private registry
- Use a git dependency with a URL
- Move the dependency into the workspace",
        examples: ExamplePair {
            before: r#"[dependencies]
# Absolute path - not portable
my-lib = { path = "/home/user/code/my-lib" }

# Escapes repo root
other-lib = { path = "../../../outside-repo/lib" }"#,
            after: r#"[dependencies]
# Repo-relative path
my-lib = { path = "../my-lib" }

# Or use a git/registry dependency for external code
other-lib = { git = "https://github.com/org/other-lib" }"#,
        },
    }
}

fn explain_workspace_inheritance() -> Explanation {
    Explanation {
        title: "Workspace Dependency Inheritance",
        description: "\
Detects dependencies that exist in [workspace.dependencies] but are not using
`workspace = true` inheritance.

When a workspace defines shared dependencies in [workspace.dependencies], member
crates should inherit them to ensure:
- Consistent versions across all workspace crates
- Single source of truth for dependency versions
- Easier bulk updates when upgrading dependencies
- Reduced duplication in Cargo.toml files",
        remediation: "\
Change the dependency declaration to use workspace inheritance:

    # In member crate's Cargo.toml
    [dependencies]
    serde.workspace = true

You can still add local features while inheriting the version:

    serde = { workspace = true, features = [\"derive\"] }

If you intentionally need a different version, add the dependency to the
check's allow list in depguard.toml.",
        examples: ExamplePair {
            before: r#"# In Cargo.toml (workspace root)
[workspace.dependencies]
serde = "1.0"

# In crates/my-crate/Cargo.toml
[dependencies]
serde = "1.0"  # Duplicates workspace definition"#,
            after: r#"# In Cargo.toml (workspace root)
[workspace.dependencies]
serde = "1.0"

# In crates/my-crate/Cargo.toml
[dependencies]
serde.workspace = true

# Or with additional features:
serde = { workspace = true, features = ["derive"] }"#,
        },
    }
}

fn explain_tool_runtime() -> Explanation {
    Explanation {
        title: "Tool Runtime Error",
        description: "\
Depguard encountered an internal error or invalid environment while running.

This indicates the tool could not complete analysis due to a runtime failure
such as an invalid config file, missing repository root, or a git error.",
        remediation: "\
Fix the underlying error and re-run depguard:
- Check the error message in stderr
- Fix invalid depguard.toml syntax or values
- Ensure the repo root exists and is accessible
- Provide required git history for diff scope",
        examples: ExamplePair {
            before: r#"# Fails with a tool runtime error
depguard check --repo-root /missing/path"#,
            after: r#"# Succeeds after fixing the input
depguard check --repo-root ."#,
        },
    }
}

// --- Code-level explanations ---

fn explain_wildcard_version() -> Explanation {
    // Same as the check, but framed as the specific code
    let mut exp = explain_no_wildcards();
    exp.title = "Wildcard Version";
    exp
}

fn explain_path_without_version() -> Explanation {
    let mut exp = explain_path_requires_version();
    exp.title = "Path Without Version";
    exp
}

fn explain_absolute_path() -> Explanation {
    Explanation {
        title: "Absolute Path Dependency",
        description: "\
A dependency is declared with an absolute filesystem path.

Absolute paths like `/home/user/code/lib` or `C:\\Code\\lib` are:
- Machine-specific and not portable across systems
- Not reproducible in CI/CD environments
- Not shareable with other contributors
- A potential security concern (may leak host directory structure)",
        remediation: "\
Convert to a repo-relative path:

    my-crate = { path = \"../my-crate\" }

Or use a published/git dependency:

    my-crate = \"1.0\"
    my-crate = { git = \"https://github.com/org/my-crate\" }",
        examples: ExamplePair {
            before: r#"[dependencies]
my-lib = { path = "/home/user/projects/my-lib" }
win-lib = { path = "C:\\Code\\win-lib" }"#,
            after: r#"[dependencies]
my-lib = { path = "../my-lib" }
win-lib = { path = "../win-lib" }"#,
        },
    }
}

fn explain_parent_escape() -> Explanation {
    Explanation {
        title: "Path Escapes Repository Root",
        description: "\
A path dependency uses `..` segments that navigate outside the repository root.

This typically happens when:
- A dependency lives in a sibling directory outside the repo
- The path was copied from another project with different structure
- A monorepo was split but paths weren't updated

Dependencies outside the repository:
- Are not version-controlled with the project
- Won't exist on CI/CD machines
- Cannot be cloned by other contributors
- Break the principle of self-contained repositories",
        remediation: "\
Move the dependency into the workspace, or use an external reference:

1. Move into workspace:
   mv ../external-lib crates/external-lib
   # Update path to: { path = \"crates/external-lib\" }

2. Use git dependency:
   external-lib = { git = \"https://github.com/org/external-lib\" }

3. Publish to a registry:
   external-lib = \"1.0\"",
        examples: ExamplePair {
            before: r#"# From crates/my-app/Cargo.toml
[dependencies]
# Escapes repo: crates/my-app -> crates -> repo-root -> ??? (outside!)
shared = { path = "../../../shared-libs/common" }"#,
            after: r#"# Move shared into the workspace, then:
[dependencies]
shared = { path = "../shared" }

# Or use a git/registry dependency:
shared = { git = "https://github.com/org/shared-libs", subdirectory = "common" }"#,
        },
    }
}

fn explain_missing_workspace_true() -> Explanation {
    let mut exp = explain_workspace_inheritance();
    exp.title = "Missing workspace = true";
    exp
}

fn explain_runtime_error() -> Explanation {
    let mut exp = explain_tool_runtime();
    exp.title = "Runtime Error";
    exp
}

// --- New check explanations ---

fn explain_git_requires_version() -> Explanation {
    Explanation {
        title: "Git Dependencies Require Version",
        description: "\
Detects git dependencies in publishable crates that lack an explicit version.

When publishing a crate to crates.io, Cargo ignores the `git` key and uses only
the version from the registry. If no version is specified:
- The crate cannot be published (cargo publish will fail)
- Users who depend on your crate won't be able to build it

This check only applies to crates that can be published (publish != false).",
        remediation: "\
Add an explicit version alongside the git URL:

    my-crate = { git = \"https://github.com/org/repo\", version = \"0.1.0\" }

Alternatively, use workspace inheritance:

    my-crate.workspace = true

Or mark the crate as unpublishable in its Cargo.toml:

    [package]
    publish = false",
        examples: ExamplePair {
            before: r#"[dependencies]
my-lib = { git = "https://github.com/org/my-lib" }"#,
            after: r#"[dependencies]
my-lib = { git = "https://github.com/org/my-lib", version = "0.1.0" }

# Or use workspace inheritance:
my-lib.workspace = true"#,
        },
    }
}

fn explain_git_without_version() -> Explanation {
    let mut exp = explain_git_requires_version();
    exp.title = "Git Without Version";
    exp
}

fn explain_dev_only_in_normal() -> Explanation {
    Explanation {
        title: "Dev-Only Crate in Normal Dependencies",
        description: "\
Detects crates that are typically dev-only appearing in [dependencies].

Some crates are designed exclusively for testing and development:
- Test frameworks: proptest, quickcheck, rstest, test-case
- Mocking: mockall, mockito, wiremock
- Benchmarking: criterion, divan
- Test utilities: tempfile, assert_cmd, insta

Including these in [dependencies] instead of [dev-dependencies]:
- Increases binary size for consumers
- May add unnecessary compile time
- Suggests a potential configuration error",
        remediation: "\
Move the dependency to [dev-dependencies]:

    [dev-dependencies]
    mockall = \"0.11\"
    proptest = \"1.0\"

If you genuinely need it in [dependencies] for production code,
add it to the check's allow list in depguard.toml.",
        examples: ExamplePair {
            before: r#"[dependencies]
mockall = "0.11"
proptest = "1.0""#,
            after: r#"[dependencies]
# Production dependencies only

[dev-dependencies]
mockall = "0.11"
proptest = "1.0""#,
        },
    }
}

fn explain_dev_dep_in_normal() -> Explanation {
    let mut exp = explain_dev_only_in_normal();
    exp.title = "Dev Dependency in Normal";
    exp
}

fn explain_default_features_explicit() -> Explanation {
    Explanation {
        title: "Explicit default-features",
        description: "\
Detects dependencies with inline options that don't explicitly set default-features.

When a dependency has inline options (features, optional, path, git) but doesn't
explicitly declare `default-features = true/false`, it can lead to:
- Unclear intent about whether default features are wanted
- Accidental inclusion of unwanted features
- Inconsistent behavior when features change upstream",
        remediation: "\
Add an explicit `default-features` declaration:

    # If you want default features:
    serde = { version = \"1.0\", features = [\"derive\"], default-features = true }

    # If you don't want default features:
    tokio = { version = \"1.0\", features = [\"rt\"], default-features = false }

For simple version-only dependencies, this check doesn't apply.",
        examples: ExamplePair {
            before: r#"[dependencies]
serde = { version = "1.0", features = ["derive"] }"#,
            after: r#"[dependencies]
serde = { version = "1.0", features = ["derive"], default-features = true }"#,
        },
    }
}

fn explain_default_features_implicit() -> Explanation {
    let mut exp = explain_default_features_explicit();
    exp.title = "Default Features Implicit";
    exp
}

fn explain_no_multiple_versions() -> Explanation {
    Explanation {
        title: "No Multiple Versions",
        description: "\
Detects the same crate with different versions across workspace members.

Having multiple versions of the same dependency in a workspace:
- Increases binary size (both versions are compiled)
- Can cause subtle compatibility issues
- Makes dependency updates more complex
- May indicate accidental version drift",
        remediation: "\
Align all workspace members to use the same version:

1. Define the dependency in [workspace.dependencies]:
   [workspace.dependencies]
   serde = \"1.0.200\"

2. Use workspace inheritance in all members:
   [dependencies]
   serde.workspace = true

If intentional version differences are required, add the crate
to the check's allow list.",
        examples: ExamplePair {
            before: r#"# crates/a/Cargo.toml
[dependencies]
serde = "1.0.195"

# crates/b/Cargo.toml
[dependencies]
serde = "1.0.200""#,
            after: r#"# Cargo.toml (workspace root)
[workspace.dependencies]
serde = "1.0.200"

# crates/a/Cargo.toml
[dependencies]
serde.workspace = true

# crates/b/Cargo.toml
[dependencies]
serde.workspace = true"#,
        },
    }
}

fn explain_duplicate_different_versions() -> Explanation {
    let mut exp = explain_no_multiple_versions();
    exp.title = "Duplicate Different Versions";
    exp
}

fn explain_optional_unused() -> Explanation {
    Explanation {
        title: "Unused Optional Dependency",
        description: "\
Detects optional dependencies that aren't referenced in any feature.

When a dependency is marked `optional = true`, it should be activated by
at least one feature in the [features] table. An optional dependency that
isn't referenced in any feature:
- Cannot be enabled by users
- Suggests incomplete feature configuration
- May indicate dead code or misconfiguration",
        remediation: "\
Either reference the optional dependency in a feature:

    [features]
    my-feature = [\"dep:optional-crate\"]

Or remove the `optional = true` if it should always be included:

    [dependencies]
    my-crate = \"1.0\"  # Remove optional = true",
        examples: ExamplePair {
            before: r#"[dependencies]
serde = { version = "1.0", optional = true }

[features]
# No feature uses serde"#,
            after: r#"[dependencies]
serde = { version = "1.0", optional = true }

[features]
serialization = ["dep:serde"]"#,
        },
    }
}

fn explain_optional_not_in_features() -> Explanation {
    let mut exp = explain_optional_unused();
    exp.title = "Optional Not in Features";
    exp
}

fn explain_yanked_versions() -> Explanation {
    Explanation {
        title: "No Yanked Versions",
        description: "\
Detects dependencies pinned to versions listed as yanked in an offline index.

Yanked versions are removed from normal resolution because they often indicate:
- serious bugs discovered after publish
- accidental bad releases
- security or reliability concerns

This check only flags exact pins (`=x.y.z`) when the version appears in the supplied yanked index.",
        remediation: "\
Upgrade to a non-yanked version and keep the dependency explicitly pinned:

    serde = \"=1.0.200\"

If the yanked version is intentional for a temporary reason, document it and add
the dependency to the check allowlist.",
        examples: ExamplePair {
            before: r#"[dependencies]
serde = "=1.0.189""#,
            after: r#"[dependencies]
serde = "=1.0.200""#,
        },
    }
}

fn explain_version_yanked() -> Explanation {
    let mut exp = explain_yanked_versions();
    exp.title = "Pinned Version Is Yanked";
    exp
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lookup_by_check_id() {
        assert!(lookup_explanation(ids::CHECK_DEPS_NO_WILDCARDS).is_some());
        assert!(lookup_explanation(ids::CHECK_DEPS_PATH_REQUIRES_VERSION).is_some());
        assert!(lookup_explanation(ids::CHECK_DEPS_PATH_SAFETY).is_some());
        assert!(lookup_explanation(ids::CHECK_DEPS_WORKSPACE_INHERITANCE).is_some());
        assert!(lookup_explanation(ids::CHECK_TOOL_RUNTIME).is_some());
    }

    #[test]
    fn lookup_by_code() {
        assert!(lookup_explanation(ids::CODE_WILDCARD_VERSION).is_some());
        assert!(lookup_explanation(ids::CODE_PATH_WITHOUT_VERSION).is_some());
        assert!(lookup_explanation(ids::CODE_ABSOLUTE_PATH).is_some());
        assert!(lookup_explanation(ids::CODE_PARENT_ESCAPE).is_some());
        assert!(lookup_explanation(ids::CODE_MISSING_WORKSPACE_TRUE).is_some());
        assert!(lookup_explanation(ids::CODE_RUNTIME_ERROR).is_some());
    }

    #[test]
    fn lookup_unknown_returns_none() {
        assert!(lookup_explanation("unknown.check").is_none());
        assert!(lookup_explanation("unknown_code").is_none());
    }

    #[test]
    fn all_check_ids_are_valid() {
        for id in all_check_ids() {
            assert!(
                lookup_explanation(id).is_some(),
                "check_id {} should be in registry",
                id
            );
        }
    }

    #[test]
    fn all_codes_are_valid() {
        for code in all_codes() {
            assert!(
                lookup_explanation(code).is_some(),
                "code {} should be in registry",
                code
            );
        }
    }
}