Skip to main content

x_lint/
runner.rs

1// Copyright (c) The Diem Core Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{prelude::*, LintContext};
5use camino::Utf8Path;
6use x_core::XCoreContext;
7
8/// Configuration for the lint engine.
9#[derive(Clone, Debug)]
10pub struct LintEngineConfig<'cfg> {
11    core: &'cfg XCoreContext,
12    project_linters: &'cfg [&'cfg dyn ProjectLinter],
13    package_linters: &'cfg [&'cfg dyn PackageLinter],
14    file_path_linters: &'cfg [&'cfg dyn FilePathLinter],
15    content_linters: &'cfg [&'cfg dyn ContentLinter],
16    fail_fast: bool,
17}
18
19impl<'cfg> LintEngineConfig<'cfg> {
20    pub fn new(core: &'cfg XCoreContext) -> Self {
21        Self {
22            core,
23            project_linters: &[],
24            package_linters: &[],
25            file_path_linters: &[],
26            content_linters: &[],
27            fail_fast: false,
28        }
29    }
30
31    pub fn with_project_linters(
32        &mut self,
33        project_linters: &'cfg [&'cfg dyn ProjectLinter],
34    ) -> &mut Self {
35        self.project_linters = project_linters;
36        self
37    }
38
39    pub fn with_package_linters(
40        &mut self,
41        package_linters: &'cfg [&'cfg dyn PackageLinter],
42    ) -> &mut Self {
43        self.package_linters = package_linters;
44        self
45    }
46
47    pub fn with_file_path_linters(
48        &mut self,
49        file_path_linters: &'cfg [&'cfg dyn FilePathLinter],
50    ) -> &mut Self {
51        self.file_path_linters = file_path_linters;
52        self
53    }
54
55    pub fn with_content_linters(
56        &mut self,
57        content_linters: &'cfg [&'cfg dyn ContentLinter],
58    ) -> &mut Self {
59        self.content_linters = content_linters;
60        self
61    }
62
63    pub fn fail_fast(&mut self, fail_fast: bool) -> &mut Self {
64        self.fail_fast = fail_fast;
65        self
66    }
67
68    pub fn build(&self) -> LintEngine<'cfg> {
69        LintEngine::new(self.clone())
70    }
71}
72
73/// Executor for linters.
74#[derive(Debug)]
75pub struct LintEngine<'cfg> {
76    config: LintEngineConfig<'cfg>,
77    project_ctx: ProjectContext<'cfg>,
78}
79
80impl<'cfg> LintEngine<'cfg> {
81    pub fn new(config: LintEngineConfig<'cfg>) -> Self {
82        let project_ctx = ProjectContext::new(config.core);
83        Self {
84            config,
85            project_ctx,
86        }
87    }
88
89    pub fn run(&self) -> Result<LintResults> {
90        let mut skipped = vec![];
91        let mut messages = vec![];
92
93        // TODO: add support for file linters.
94
95        // Run project linters.
96        if !self.config.project_linters.is_empty() {
97            for linter in self.config.project_linters {
98                let source = self.project_ctx.source(linter.name());
99                let mut formatter = LintFormatter::new(source, &mut messages);
100                match linter.run(&self.project_ctx, &mut formatter)? {
101                    RunStatus::Executed => {
102                        // Lint ran successfully.
103                    }
104                    RunStatus::Skipped(reason) => {
105                        skipped.push((source, reason));
106                    }
107                }
108
109                if self.config.fail_fast && !messages.is_empty() {
110                    // At least one issue was found.
111                    return Ok(LintResults { skipped, messages });
112                }
113            }
114        }
115
116        // Run package linters.
117        if !self.config.package_linters.is_empty() {
118            let package_graph = self.project_ctx.package_graph()?;
119
120            for (workspace_path, metadata) in package_graph.workspace().iter_by_path() {
121                let package_ctx = PackageContext::new(
122                    &self.project_ctx,
123                    package_graph,
124                    workspace_path,
125                    metadata,
126                )?;
127                for linter in self.config.package_linters {
128                    let source = package_ctx.source(linter.name());
129                    let mut formatter = LintFormatter::new(source, &mut messages);
130                    match linter.run(&package_ctx, &mut formatter)? {
131                        RunStatus::Executed => {
132                            // Lint ran successfully.
133                        }
134                        RunStatus::Skipped(reason) => {
135                            skipped.push((source, reason));
136                        }
137                    }
138
139                    if self.config.fail_fast && !messages.is_empty() {
140                        // At least one issue was found.
141                        return Ok(LintResults { skipped, messages });
142                    }
143                }
144            }
145        }
146
147        // Run file path linters.
148        if !self.config.file_path_linters.is_empty() {
149            let file_list = self.file_list()?;
150
151            let file_ctxs = file_list.map(|path| FilePathContext::new(&self.project_ctx, path));
152
153            for file_ctx in file_ctxs {
154                for linter in self.config.file_path_linters {
155                    let source = file_ctx.source(linter.name());
156                    let mut formatter = LintFormatter::new(source, &mut messages);
157                    match linter.run(&file_ctx, &mut formatter)? {
158                        RunStatus::Executed => {
159                            // Lint ran successfully.
160                        }
161                        RunStatus::Skipped(reason) => {
162                            skipped.push((source, reason));
163                        }
164                    }
165
166                    if self.config.fail_fast && !messages.is_empty() {
167                        // At least one issue was found.
168                        return Ok(LintResults { skipped, messages });
169                    }
170                }
171            }
172        }
173
174        // Run content linters.
175        if !self.config.content_linters.is_empty() {
176            let file_list = self.file_list()?;
177
178            // TODO: This should probably be a worker queue with a thread pool or something.
179
180            let file_ctxs = file_list.map(|path| FilePathContext::new(&self.project_ctx, path));
181
182            for file_ctx in file_ctxs {
183                let linters_to_run = self
184                    .config
185                    .content_linters
186                    .iter()
187                    .copied()
188                    .filter_map(|linter| match linter.pre_run(&file_ctx) {
189                        Ok(RunStatus::Executed) => Some(Ok(linter)),
190                        Ok(RunStatus::Skipped(reason)) => {
191                            let source = file_ctx.source(linter.name());
192                            skipped.push((source, reason));
193                            None
194                        }
195                        Err(err) => Some(Err(err)),
196                    })
197                    .collect::<Result<Vec<_>>>()?;
198
199                if linters_to_run.is_empty() {
200                    // No linters to run for this file -- no point loading it.
201                    continue;
202                }
203
204                // Load up the content for this file.
205                let content_ctx = match file_ctx.load()? {
206                    Some(content_ctx) => content_ctx,
207                    None => {
208                        // This file is missing -- can't run content linters on it.
209                        continue;
210                    }
211                };
212
213                for linter in linters_to_run {
214                    let source = content_ctx.source(linter.name());
215                    let mut formatter = LintFormatter::new(source, &mut messages);
216
217                    match linter.run(&content_ctx, &mut formatter)? {
218                        RunStatus::Executed => {
219                            // Yay! Lint ran successfully.
220                        }
221                        RunStatus::Skipped(reason) => {
222                            skipped.push((source, reason));
223                        }
224                    }
225
226                    if self.config.fail_fast && !messages.is_empty() {
227                        // At least one issue was found.
228                        return Ok(LintResults { skipped, messages });
229                    }
230                }
231            }
232        }
233
234        Ok(LintResults { skipped, messages })
235    }
236
237    // ---
238    // Helper methods
239    // ---
240
241    fn file_list(&self) -> Result<impl Iterator<Item = &'cfg Utf8Path> + 'cfg> {
242        let tracked_files = self.config.core.git_cli().tracked_files()?;
243        // TODO: make global exclusions configurable
244        Ok(tracked_files
245            .iter()
246            .filter(|f| !f.starts_with("testsuite/diem-fuzzer/artifacts/")))
247    }
248}
249
250#[derive(Debug)]
251#[non_exhaustive]
252pub struct LintResults<'l> {
253    pub skipped: Vec<(LintSource<'l>, SkipReason<'l>)>,
254    pub messages: Vec<(LintSource<'l>, LintMessage)>,
255}