Skip to main content

rust_doctor/
lib.rs

1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
2
3use std::path::Path;
4
5mod audit;
6mod baseline;
7mod bounded_read;
8mod cargo_health;
9mod configuration;
10mod delta;
11mod execution;
12mod git;
13mod git_scope;
14mod internal_error;
15#[cfg(test)]
16mod permutations;
17mod policy;
18pub mod presentation;
19pub mod render;
20mod repo_hygiene;
21mod report;
22mod scan_target;
23pub mod score_block;
24mod source_kernel;
25mod source_text;
26mod structure;
27#[cfg(test)]
28mod test_clock;
29#[cfg(test)]
30#[path = "test_scratch.rs"]
31mod test_scratch;
32pub mod terminal_text;
33mod workspace_path;
34
35pub use audit::{
36    Audit, AuditCategory, AuditCategoryName, AuditScore, SCORE_MODEL, ScoreDimensions, ScoreLabel,
37    SeverityCounts, ShareError,
38};
39pub use delta::{DeltaMatch, DeltaReport, DeltaSummary};
40pub use git_scope::{ExecutionScope, ScopeMode, ScopeReport};
41pub use policy::{
42    BlockingLevel, BlockingLevelSource, CatalogEntry, CategoryOverride, RuleLevel, RuleLevelSource,
43    RuleOverride, RuleTier, catalog,
44};
45/// The file length `rust_doctor::structure::oversized_unit` reports at.
46///
47/// Published because the binary crate holds the same bound over its own files
48/// and must not restate the number: `src/tui/` and `src/handoff.rs` are
49/// compiled into the binary, not the library, and a second `1000` written
50/// there is a second place to keep in step with the rule.
51pub use structure::FILE_LINES as OVERSIZED_UNIT_FILE_LINES;
52pub use report::{
53    Diagnostic, DiagnosticSource, DiagnosticSpan, GateReport, GateStatus, InspectReport,
54    InspectRequest, PackageReport, PolicyBlockingReport, PolicyReport, PolicyRuleReport,
55    ProjectReport, RelatedLocation, ReportError, SCHEMA_VERSION, ScanReport, Severity, Status,
56    Summary, ToolchainReport,
57};
58
59pub fn inspect(request: InspectRequest) -> InspectReport {
60    match InspectionSession::prepare(request) {
61        Ok(session) => session.inspect(),
62        Err(report) => *report,
63    }
64}
65
66#[derive(Debug)]
67pub struct InspectionSession {
68    prepared: execution::PreparedInspection,
69    plan: policy::PolicyPlan,
70    scope: git_scope::ValidatedScope,
71}
72
73impl InspectionSession {
74    pub fn prepare(request: InspectRequest) -> Result<Self, Box<InspectReport>> {
75        let policy = request.policy().clone();
76        Self::prepare_with(request, &policy)
77    }
78
79    fn prepare_with(
80        request: InspectRequest,
81        policy: &policy::PolicyInput,
82    ) -> Result<Self, Box<InspectReport>> {
83        // The overrides are read once, before any path is discovered and any
84        // process is started. What comes back is what the plan compiles from,
85        // so the compilation below cannot fail a second time over the same
86        // answer.
87        let validated = match policy.validate() {
88            Ok(validated) => validated,
89            Err(error) => {
90                return Err(Box::new(report::policy_failure(
91                    error,
92                    policy.failure_blocking(),
93                )));
94            }
95        };
96        // The base selector is checked once, here, and resolution takes what
97        // came back rather than the request, so it cannot fail a second time
98        // over the same answer.
99        let scope = match request.scope().validate() {
100            Ok(scope) => scope,
101            Err(error) => {
102                return Err(Box::new(report::scope_failure(
103                    error,
104                    policy.failure_blocking(),
105                )));
106            }
107        };
108        let prepared = match execution::prepare(&request.path) {
109            Ok(prepared) => prepared,
110            Err(result) => {
111                return Err(Box::new(report::preparation_failure(
112                    *result,
113                    policy.failure_blocking(),
114                )));
115            }
116        };
117        let plan =
118            policy::PolicyPlan::compile_with_configuration(&validated, &prepared.configuration);
119        Ok(Self {
120            prepared,
121            plan,
122            scope,
123        })
124    }
125
126    pub fn workspace_root(&self) -> &Path {
127        self.prepared.workspace_root()
128    }
129
130    pub fn inspect(self) -> InspectReport {
131        inspect_prepared(self)
132    }
133}
134
135#[cfg(test)]
136fn inspect_with(request: InspectRequest, policy: &policy::PolicyInput) -> InspectReport {
137    match InspectionSession::prepare_with(request, policy) {
138        Ok(session) => session.inspect(),
139        Err(report) => *report,
140    }
141}
142
143fn inspect_prepared(session: InspectionSession) -> InspectReport {
144    let InspectionSession {
145        prepared,
146        plan,
147        scope: requested,
148    } = session;
149    let scope = match git_scope::resolve(&requested, prepared.workspace_root()) {
150        Ok(scope) => scope,
151        Err(error) => {
152            return report::preparation_failure(prepared.fail(error), plan.blocking());
153        }
154    };
155    if let git_scope::ResolvedScope::Baseline { comparison_base } = scope.kind() {
156        let snapshot = match baseline::materialize(prepared.workspace_root(), comparison_base) {
157            Ok(snapshot) => snapshot,
158            Err(error) => {
159                return report::from_execution_scoped(prepared.fail(error), &plan, scope);
160            }
161        };
162        let execution =
163            execution::execute_baseline(prepared, snapshot.workspace(), snapshot.target(), &plan);
164        let report = report::from_baseline_execution(execution, &plan, scope);
165        return match snapshot.cleanup() {
166            Ok(()) => report,
167            Err(error) => report::baseline_report_failure(report, error),
168        };
169    }
170    report::from_execution_scoped(execution::execute(prepared, &plan), &plan, scope)
171}
172
173#[cfg(test)]
174mod tests {
175    use std::path::Path;
176
177    use super::*;
178    use crate::policy::{PolicyInput, RuleLevel};
179
180    #[test]
181    fn invalid_policy_returns_before_execution_or_path_discovery() {
182        let request = InspectRequest::new("/path/that/must/not/be/inspected");
183        let policy = PolicyInput::default().with_rule("unknown::rule", RuleLevel::Warn);
184
185        let report = inspect_with(request, &policy);
186
187        assert_eq!(report.status, Status::Failed);
188        assert!(!report.complete);
189        assert!(report.project.is_none());
190        assert!(report.scan.command.is_none());
191        assert!(report.diagnostics.is_empty());
192        assert_eq!(report.errors.len(), 1);
193        assert_eq!(report.errors[0].stage, "policy");
194        assert_eq!(report.errors[0].code, "unknown-rule");
195    }
196
197    #[test]
198    fn clippy_off_prunes_execution_and_error_preserves_warning_identity() {
199        let fixture =
200            Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/kernel-contract/todo");
201        let baseline = inspect_with(InspectRequest::new(&fixture), &PolicyInput::default());
202        assert_eq!(baseline.status, Status::Complete);
203        let baseline_ids: Vec<_> = baseline
204            .diagnostics
205            .iter()
206            .filter(|diagnostic| diagnostic.code.as_deref() == Some("clippy::todo"))
207            .map(|diagnostic| diagnostic.id.clone())
208            .collect();
209        assert!(!baseline_ids.is_empty());
210
211        let off_policy = PolicyInput::default().with_rule("clippy::todo", RuleLevel::Off);
212        let off = inspect_with(InspectRequest::new(&fixture), &off_policy);
213        assert_eq!(off.status, Status::Complete);
214        assert!(
215            off.diagnostics
216                .iter()
217                .all(|diagnostic| diagnostic.code.as_deref() != Some("clippy::todo"))
218        );
219        let off_command = off
220            .scan
221            .command
222            .expect("complete scan should expose its command");
223        assert!(
224            !off_command
225                .iter()
226                .any(|argument| argument == "clippy::todo")
227        );
228
229        let error_policy = PolicyInput::default().with_rule("clippy::todo", RuleLevel::Error);
230        let error = inspect_with(InspectRequest::new(&fixture), &error_policy);
231        assert_eq!(error.status, Status::Complete);
232        let error_ids: Vec<_> = error
233            .diagnostics
234            .iter()
235            .filter(|diagnostic| diagnostic.code.as_deref() == Some("clippy::todo"))
236            .map(|diagnostic| {
237                assert_eq!(diagnostic.base_severity, Severity::Warning);
238                assert_eq!(diagnostic.severity, Severity::Error);
239                diagnostic.id.clone()
240            })
241            .collect();
242        assert_eq!(error_ids, baseline_ids);
243        assert_eq!(error.gate.status, GateStatus::Failed);
244        assert_eq!(error.gate.blocking_diagnostics, Some(error_ids.len()));
245        let error_command = error
246            .scan
247            .command
248            .expect("complete scan should expose its command");
249        assert!(
250            error_command
251                .windows(2)
252                .any(|pair| pair == ["-W", "clippy::todo"])
253        );
254        assert!(!error_command.iter().any(|argument| argument == "-D"));
255    }
256}