omnicoach 0.3.0

Lightweight, high-signal static analysis and command linter for Python and Rust
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
//! Rule taxonomy: the facets each rule declares.
//!
//! Pure, constant metadata with no queries. Rules and runners never branch on it.

use strum::{EnumIter, EnumMessage, IntoStaticStr};

/// The subject of a rule: the only multi-valued, hierarchical facet.
///
/// Each topic is declared once, as an associated `const` on [`Topic`]. The consts are
/// `pub(crate)` so that a topic no rule uses fails the `dead_code` lint.
///
/// Admit a topic only when it (1) names a subject (not a quality), (2) passes the
/// all-and-some test under its parent, (3) has a `description` and an includes/excludes
/// `scope_note`, (4) has a unique `kebab-case` label, and (5) has at least one rule
/// (`docs/dev/tag_guide.md` §4).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Topic {
    /// The topic's canonical `kebab-case` label, e.g. `test-timing`.
    pub label: &'static str,
    /// The broader topic, if any. Its type makes a parent in another facet impossible,
    /// and `const` evaluation makes a cycle a compile error (`E0391`).
    pub parent: Option<&'static Self>,
    /// One-line description of the topic.
    pub description: &'static str,
    /// What belongs under the topic and what does not.
    pub scope_note: &'static str,
    /// Other labels that select the same topic. Only the canonical label is ever displayed.
    pub synonyms: &'static [&'static str],
}

impl Topic {
    /// How identifiers are named.
    pub(crate) const NAMING: Self = Self {
        label: "naming",
        parent: None,
        description: "How identifiers are named.",
        scope_note: "Identifier names. Not string contents, file names, formatting or layout.",
        synonyms: &[],
    };

    /// Too short or cryptic names: abbreviations and single letters.
    pub(crate) const ABBREVIATED_NAMES: Self = Self {
        label: "abbreviated-names",
        parent: Some(&Self::NAMING),
        description: "Too short or cryptic names: abbreviations and single letters.",
        scope_note: "Abbreviations and single-letter identifiers. Not type or unit suffixes.",
        synonyms: &[],
    };

    /// Names encoding a type, container or unit.
    pub(crate) const TYPE_ENCODED_NAMES: Self = Self {
        label: "type-encoded-names",
        parent: Some(&Self::NAMING),
        description: "Names encoding a type, container or unit.",
        scope_note: "Type, container or unit encoded in the name (`users_dict`, `timeout_secs`). \
                     Not names that are just short.",
        synonyms: &[],
    };

    /// Practices specific to test code.
    pub(crate) const TESTING: Self = Self {
        label: "testing",
        parent: None,
        description: "Practices specific to test code.",
        scope_note: "How test code is structured and written. Not production code, even when \
                     tested.",
        synonyms: &[],
    };

    /// Count, shape and granularity of test assertions.
    pub(crate) const TEST_ASSERTIONS: Self = Self {
        label: "test-assertions",
        parent: Some(&Self::TESTING),
        description: "Count, shape and granularity of test assertions.",
        scope_note: "Number and shape of test assertions. Not production `assert` or what the \
                     test exercises.",
        synonyms: &[],
    };

    /// Mocks, fakes, stubs, spies and monkeypatching.
    pub(crate) const TEST_DOUBLES: Self = Self {
        label: "test-doubles",
        parent: Some(&Self::TESTING),
        description: "Mocks, fakes, stubs, spies and monkeypatching.",
        scope_note: "Replacing collaborators in tests and asserting on those replacements. Not \
                     fixtures that only build data.",
        synonyms: &[],
    };

    /// Sleeps, clocks and timeouts in tests.
    pub(crate) const TEST_TIMING: Self = Self {
        label: "test-timing",
        parent: Some(&Self::TESTING),
        description: "Sleeps, clocks and timeouts in tests.",
        scope_note: "Sleeping or waiting in tests. Not production retries or duration \
                     representation (see `durations`).",
        synonyms: &[],
    };

    /// What the static type checker can see and verify.
    pub(crate) const STATIC_TYPING: Self = Self {
        label: "static-typing",
        parent: None,
        description: "What the static type checker can see and verify.",
        scope_note: "What static type checking can prove. Not runtime validation or dataclass \
                     mutability (see `record-types`).",
        synonyms: &[],
    };

    /// Code that overrides or routes around the type checker.
    pub(crate) const TYPE_CHECKER_BYPASS: Self = Self {
        label: "type-checker-bypass",
        parent: Some(&Self::STATIC_TYPING),
        description: "Code that overrides or routes around the type checker.",
        scope_note: "Casts and dynamic access (`cast`, `getattr`) that hide types from the \
                     checker. Not `omni:` directives.",
        synonyms: &[],
    };

    /// Meaning carried by position instead of a name.
    pub(crate) const POSITIONAL_MEANING: Self = Self {
        label: "positional-meaning",
        parent: None,
        description: "Meaning carried by position instead of a name.",
        scope_note: "Meaning carried by argument order or tuple position instead of a name. Not \
                     named-field access.",
        synonyms: &[],
    };

    /// Reading sequence elements by literal index instead of unpacking.
    pub(crate) const POSITIONAL_INDEXING: Self = Self {
        label: "positional-indexing",
        parent: Some(&Self::POSITIONAL_MEANING),
        description: "Reading sequence elements by literal index instead of unpacking.",
        scope_note: "Access by literal index (`t[0]`) where destructuring would name the parts. \
                     Not loops over indices or slicing.",
        synonyms: &[],
    };

    /// Version-control operations and history.
    pub(crate) const VCS: Self = Self {
        label: "vcs",
        parent: None,
        description: "Version-control operations and history.",
        scope_note: "Commands and workflows of any version-control system. Not CI or code review.",
        synonyms: &["version-control"],
    };

    /// Rules specific to Jujutsu.
    pub(crate) const JJ: Self = Self {
        label: "jj",
        parent: Some(&Self::VCS),
        description: "Rules specific to Jujutsu.",
        scope_note: "Commands and workflows specific to Jujutsu. Not generic VCS behaviour.",
        synonyms: &["jujutsu"],
    };

    /// How spans of time are represented.
    pub(crate) const DURATIONS: Self = Self {
        label: "durations",
        parent: None,
        description: "How spans of time are represented.",
        scope_note: "Representing lengths of time and their units. Not sleeping/waiting (see \
                     `test-timing`) or wall-clock dates.",
        synonyms: &[],
    };

    /// Declaring named records (`dataclass`, `NamedTuple`, `struct`).
    pub(crate) const RECORD_TYPES: Self = Self {
        label: "record-types",
        parent: None,
        description: "Declaring named records (`dataclass`, `NamedTuple`, `struct`).",
        scope_note: "Field-bundle declarations, mutability and slots. Not enums or protocols.",
        synonyms: &[],
    };

    /// How literal values are written in code.
    pub(crate) const LITERALS: Self = Self {
        label: "literals",
        parent: None,
        description: "How literal values are written in code.",
        scope_note: "Writing string and number literals (multiline strings, format placeholders, \
                     magic numbers). Not identifiers or logging calls (see `logging`).",
        synonyms: &[],
    };

    /// Nesting depth and scope structure of code units.
    pub(crate) const NESTING: Self = Self {
        label: "nesting",
        parent: None,
        description: "Nesting depth and scope structure of code units.",
        scope_note: "Nested functions, closures and scope depth. Not naming or \"readability\" \
                     in general.",
        synonyms: &[],
    };

    /// Process-wide state read or written implicitly.
    pub(crate) const GLOBAL_STATE: Self = Self {
        label: "global-state",
        parent: None,
        description: "Process-wide state read or written implicitly.",
        scope_note: "Reading or writing ambient process-wide state (environment variables, \
                     globals). Not file or network I/O.",
        synonyms: &[],
    };

    /// Raising, catching, swallowing and reporting errors.
    pub(crate) const ERROR_HANDLING: Self = Self {
        label: "error-handling",
        parent: None,
        description: "Raising, catching, swallowing and reporting errors.",
        scope_note: "Exceptions and `Result`s, including `contextlib.suppress`. Not `omni:` \
                     directives (see `suppression-directives`).",
        synonyms: &[],
    };

    /// Use of logging APIs.
    pub(crate) const LOGGING: Self = Self {
        label: "logging",
        parent: None,
        description: "Use of logging APIs.",
        scope_note: "Log calls and their arguments. Not `print` or metrics.",
        synonyms: &[],
    };

    /// Concurrent execution: coroutines, tasks and threads.
    pub(crate) const CONCURRENCY: Self = Self {
        label: "concurrency",
        parent: None,
        description: "Concurrent execution: coroutines, tasks and threads.",
        scope_note: "Spawning, awaiting and joining concurrent work and its lifetimes. Not \
                     sleeping or waiting in tests (see `test-timing`).",
        synonyms: &[],
    };

    /// Hygiene of suppression directives.
    pub(crate) const SUPPRESSION_DIRECTIVES: Self = Self {
        label: "suppression-directives",
        parent: None,
        description: "Hygiene of suppression directives.",
        scope_note: "`omni:` directives that silence Omni, and the reasons given on third-party \
                     Python ones (`# noqa`, `# type: ignore`, ...). Not `contextlib.suppress` \
                     (see `error-handling`).",
        synonyms: &[],
    };

    /// Order in which declarations appear in a scope.
    pub(crate) const DECLARATION_ORDER: Self = Self {
        label: "declaration-order",
        parent: None,
        description: "Order in which declarations appear in a scope.",
        scope_note: "Ordering of functions, methods and items within a module or class. Not \
                     nesting depth (see `nesting`) or naming.",
        synonyms: &[],
    };

    /// Presence, content and sections of docstrings.
    pub(crate) const DOCSTRINGS: Self = Self {
        label: "docstrings",
        parent: None,
        description: "Presence, content and sections of docstrings.",
        scope_note: "Whether a docstring is present, what it says and how its Google-style \
                     sections (`Args:`, `Returns:`, `Attributes:`) are written. Not comments or \
                     `omni:` directives.",
        synonyms: &["documentation"],
    };
}

/// Precision facet: can the rule flag correct code?
///
/// Precision concerns false positives only: an `exact` rule may still miss violations.
///
/// **Test:** does the rule's *design* use a syntactic proxy for the target construct or its
/// exemption boundary, so that it can flag code that is correct? If yes → [`Self::Heuristic`].
/// Worked examples and edge cases: `docs/dev/tag_guide.md` §2.2.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, EnumIter, IntoStaticStr, EnumMessage,
)]
#[strum(serialize_all = "kebab-case")]
pub enum Precision {
    /// Flags only code that breaks the rule; may still miss violations.
    Exact,
    /// Uses a proxy that can flag correct code.
    Heuristic,
}

/// Consensus facet: do reasonable people disagree with the rule?
///
/// **Test:** name one ordinary situation where the flagged code is correct and appropriate.
/// If you can → [`Self::Opinionated`]. Worked examples: `docs/dev/tag_guide.md` §2.3.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, EnumIter, IntoStaticStr, EnumMessage,
)]
#[strum(serialize_all = "kebab-case")]
pub enum Consensus {
    /// There is an ordinary situation where the flagged code is correct and appropriate.
    Opinionated,
    /// The flagged code is wrong in every ordinary situation.
    Unopinionated,
}

/// Impacted quality facet (ISO/IEC 25010): what software quality suffers when violated.
///
/// **Test:** does the flagged code or command have a *mechanism that leads to wrong
/// behaviour* (a bug, a flaky or false-green test, a wrong suppression, a command that
/// corrupts state or history)? If yes → [`Self::Reliability`]. Worked examples and when to
/// add another ISO/IEC 25010 value: `docs/dev/tag_guide.md` §2.4.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, EnumIter, IntoStaticStr, EnumMessage,
)]
#[strum(serialize_all = "kebab-case")]
pub enum ImpactedQuality {
    /// Prevents a mechanism leading to wrong behaviour (bug, false-green test, wrong suppression).
    Reliability,
    /// Keeps code readable and changeable.
    Maintainability,
}

/// A rule's declared classification: one mandatory field per declared facet.
///
/// Derived facets (languages, analyzed input, file scope) have no field, and `topics` lists
/// only the most specific topics. How to classify a rule, and when to add a [`Topic`], is
/// in `docs/dev/tag_guide.md` §2 and §4.
///
/// A valid classification:
/// ```
/// use omnicoach::rule_declaration::{Classification, Consensus, ImpactedQuality, Precision, Topic};
/// const TIMING: Topic = Topic {
///     label: "timing",
///     parent: None,
///     description: "Timing.",
///     scope_note: "Timing.",
///     synonyms: &[],
/// };
/// const OK: Classification = Classification {
///     topics: &[TIMING],
///     precision: Precision::Exact,
///     consensus: Consensus::Unopinionated,
///     impacted_quality: ImpactedQuality::Reliability,
/// };
/// ```
///
/// # Compile-time guarantees
///
/// Each example below is a classification mistake that must fail to compile. `cargo test`
/// runs them as `compile_fail` doctests and checks the expected error code.
///
/// A single-valued facet is missing:
/// ```compile_fail,E0063
/// # use omnicoach::rule_declaration::{Classification, Consensus, Precision};
/// const MISSING: Classification = Classification {
///     topics: &[],
///     precision: Precision::Exact,
///     consensus: Consensus::Unopinionated,
/// };
/// ```
///
/// A single-valued facet gets two values:
/// ```compile_fail,E0062
/// # use omnicoach::rule_declaration::{Classification, Consensus, ImpactedQuality, Precision};
/// const TWICE: Classification = Classification {
///     topics: &[],
///     precision: Precision::Exact,
///     precision: Precision::Heuristic,
///     consensus: Consensus::Unopinionated,
///     impacted_quality: ImpactedQuality::Reliability,
/// };
/// ```
///
/// A facet value where a topic belongs:
/// ```compile_fail,E0308
/// # use omnicoach::rule_declaration::{Classification, Consensus, ImpactedQuality, Precision};
/// const MISPLACED: Classification = Classification {
///     topics: &[Precision::Heuristic],
///     precision: Precision::Exact,
///     consensus: Consensus::Unopinionated,
///     impacted_quality: ImpactedQuality::Reliability,
/// };
/// ```
///
/// A derived facet declared by hand:
/// ```compile_fail,E0560
/// # use omnicoach::rule_declaration::{Classification, Consensus, ImpactedQuality, Precision};
/// const DERIVED: Classification = Classification {
///     topics: &[],
///     precision: Precision::Exact,
///     consensus: Consensus::Unopinionated,
///     impacted_quality: ImpactedQuality::Reliability,
///     languages: &["python"],
/// };
/// ```
///
/// A topic that does not exist:
/// ```compile_fail,E0599
/// # use omnicoach::rule_declaration::{Classification, Consensus, ImpactedQuality, Precision, Topic};
/// const TYPO: Classification = Classification {
///     topics: &[Topic::TESING],
///     precision: Precision::Exact,
///     consensus: Consensus::Unopinionated,
///     impacted_quality: ImpactedQuality::Reliability,
/// };
/// ```
///
/// A cycle in `Topic` parent links:
/// ```compile_fail,E0391
/// # use omnicoach::rule_declaration::Topic;
/// const A: Topic = Topic {
///     label: "a",
///     parent: Some(&B),
///     description: "A.",
///     scope_note: "A.",
///     synonyms: &[],
/// };
/// const B: Topic = Topic {
///     label: "b",
///     parent: Some(&A),
///     description: "B.",
///     scope_note: "B.",
///     synonyms: &[],
/// };
/// # let _ = (A, B);
/// ```
///
/// A topic's parent in another facet:
/// ```compile_fail,E0308
/// # use omnicoach::rule_declaration::{Precision, Topic};
/// const PARENT: Topic = Topic {
///     label: "bad",
///     parent: Some(&Precision::Exact),
///     description: "Bad.",
///     scope_note: "Bad.",
///     synonyms: &[],
/// };
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Classification {
    /// The most specific topics; never a topic together with its ancestor.
    pub topics: &'static [Topic],
    /// Precision value.
    pub precision: Precision,
    /// Consensus value.
    pub consensus: Consensus,
    /// Impacted quality value.
    pub impacted_quality: ImpactedQuality,
}