1use crate::{prelude::*, LintContext};
5use camino::Utf8Path;
6use x_core::XCoreContext;
7
8#[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#[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 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 }
104 RunStatus::Skipped(reason) => {
105 skipped.push((source, reason));
106 }
107 }
108
109 if self.config.fail_fast && !messages.is_empty() {
110 return Ok(LintResults { skipped, messages });
112 }
113 }
114 }
115
116 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 }
134 RunStatus::Skipped(reason) => {
135 skipped.push((source, reason));
136 }
137 }
138
139 if self.config.fail_fast && !messages.is_empty() {
140 return Ok(LintResults { skipped, messages });
142 }
143 }
144 }
145 }
146
147 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 }
161 RunStatus::Skipped(reason) => {
162 skipped.push((source, reason));
163 }
164 }
165
166 if self.config.fail_fast && !messages.is_empty() {
167 return Ok(LintResults { skipped, messages });
169 }
170 }
171 }
172 }
173
174 if !self.config.content_linters.is_empty() {
176 let file_list = self.file_list()?;
177
178 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 continue;
202 }
203
204 let content_ctx = match file_ctx.load()? {
206 Some(content_ctx) => content_ctx,
207 None => {
208 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 }
221 RunStatus::Skipped(reason) => {
222 skipped.push((source, reason));
223 }
224 }
225
226 if self.config.fail_fast && !messages.is_empty() {
227 return Ok(LintResults { skipped, messages });
229 }
230 }
231 }
232 }
233
234 Ok(LintResults { skipped, messages })
235 }
236
237 fn file_list(&self) -> Result<impl Iterator<Item = &'cfg Utf8Path> + 'cfg> {
242 let tracked_files = self.config.core.git_cli().tracked_files()?;
243 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}