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
//! What:
//! Read what one in-process `monochromatic-lint` fix run produced:
//! the fixed source and the findings it could not fix,
//! or the reason the run cannot be trusted.
//! Why:
//! The linter's own contract (`package/linter/monochromatic-lint/src/run_embedded.rs`) is the executable's
//! `--stdin --fix` run with its one-rule configuration: status 0 with the fixed source and warning findings,
//! because the policy's configuration gives the rule severity `warn`;
//! 1 only for error findings or a warning limit,
//! neither of which that configuration can produce;
//! and 2 for a run that could not finish.
//! Anything else,
//! and any finding that is not a warning of a selected rule with a position,
//! means the run cannot be trusted,
//! so the policy fails instead of passing an unchecked file.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const run = interpretEmbeddedRun({ input, rules, outcome }); // throws string
//! ```
/// Import the selectable rules and their configuration spelling.
use super::config_schema::{MarkdownRule, markdown_rule_name};
/// Import the linter's in-process result types and finding model.
use monochromatic_lint::{
diagnostic::{Diagnostic, Severity},
run_embedded::{EmbeddedFailure, EmbeddedRun},
};
/// The longest explanation quoted from the linter's own words,
/// in characters.
pub const EXPLANATION_LIMIT: usize = 400;
/// The most bytes of fixed source the policy accepts from one run:
/// the bound the former child process had on its standard output,
/// which allows a large file whose every link grows into an object URL.
pub const MAX_FIXED_BYTES: usize = 64 << 20;
/// The most bytes of findings the policy accepts from one run:
/// the bound the former child process had on its standard error.
/// A finding counts its message, code and file name.
pub const MAX_FINDINGS_BYTES: usize = 4 << 20;
/// The status the linter reports for a run that could not finish.
pub const INCOMPLETE_STATUS: u8 = 2;
/// What:
/// One finding the linter reported and could not fix.
/// `u64` holds the one-based
/// line and column the linter gives.
/// Why:
/// The policy reports it with the incumbent's words,
/// naming the rule as configured.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type RemainingFinding = { rule: MarkdownRule; line: number; column: number; message: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemainingFinding {
/// The rule that reported it.
pub rule: MarkdownRule,
/// One-based line of the finding's first label.
pub line: u64,
/// One-based column of that label,
/// in the linter's units.
pub column: u64,
/// The linter's explanation.
pub message: String,
}
/// What:
/// A run the policy can use:
/// the source after every fix,
/// and what is left.
/// Why:
/// The fixed bytes equal the input exactly when nothing changed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type LintRun = { fixed: Uint8Array; remaining: RemainingFinding[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LintRun {
/// The fixed source,
/// byte for byte as the linter produced it.
pub fixed: Vec<u8>,
/// Findings the fixes did not remove,
/// in the linter's order.
pub remaining: Vec<RemainingFinding>,
}
/// What:
/// The selected rule a finding code names.
/// `&[MarkdownRule]` borrows the rules the
/// configuration selected.
/// Why:
/// The linter prefixes every Markdown rule with `markdown/`;
/// a code of a rule the
/// one-rule configuration did not select means the linter ran something else.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const selectedRule = (code, rules) => rules.find(rule => code === `markdown/${ruleName(rule)}`);
/// ```
fn selected_rule(code: &str, rules: &[MarkdownRule]) -> Option<MarkdownRule> {
for rule in rules {
if code.strip_prefix("markdown/") == Some(markdown_rule_name(*rule)) {
return Some(*rule);
}
}
return None;
}
/// What:
/// A one-based line or column as the whole number the policy reports.
/// Why:
/// A count too large for the type is not a position, so the run is refused.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function position(name: string, value: number): number; // throws string
/// ```
fn position(name: &str, value: usize) -> Result<u64, String> {
match u64::try_from(value) {
Ok(count) => return Ok(count),
Err(_) => return Err(format!("its {name} is not a position")),
}
}
/// What:
/// Read one finding of a successful run.
/// Why:
/// Every finding must be a warning of a selected rule with a position;
/// anything else is a contract the policy cannot rely on.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function remainingFinding(finding: Diagnostic, rules: MarkdownRule[]): RemainingFinding; // throws string
/// ```
fn remaining_finding(finding: &Diagnostic, rules: &[MarkdownRule]) -> Result<RemainingFinding, String> {
let Some(rule) = selected_rule(finding.code.as_str(), rules) else {
return Err(format!("its code {:?} is not a selected rule", finding.code));
};
if finding.severity != Severity::Warn {
return Err(String::from("its severity is not the configured \"warn\""));
}
let Some(label) = finding.labels.first() else {
return Err(String::from("it has no label with a position"));
};
return Ok(RemainingFinding {
rule,
line: position("line", label.span.line)?,
column: position("column", label.span.column)?,
message: finding.message.clone(),
});
}
/// What:
/// The linter's own explanation of a failed run,
/// cut at `EXPLANATION_LIMIT` characters.
/// Why:
/// The person needs the linter's reason;
/// the file name is left out,
/// so the explanation does not repeat the candidate's pathname the event already names.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function explanation(text: string): string;
/// ```
pub fn explanation(text: &str) -> String {
if text.chars().count() <= EXPLANATION_LIMIT {
return String::from(text);
}
let kept: String = text.chars().take(EXPLANATION_LIMIT).collect();
return format!("{kept}...");
}
/// The running size of the findings after adding `finding`.
fn add_finding_bytes(total: usize, finding: &Diagnostic) -> usize {
return total
.saturating_add(finding.message.len())
.saturating_add(finding.code.len())
.saturating_add(finding.filename.len());
}
/// What:
/// Interpret one finished in-process run over `input`.
/// `Result<LintRun, String>` is the
/// usable run,
/// or the reason it cannot be used,
/// for the person who ran the command.
/// Why:
/// See the module comment.
/// A successful run must produce text,
/// and must produce some for a non-empty input:
/// the linter refuses to empty a file,
/// so an empty fixed source would mean it never read the input,
/// and using it would empty the file.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function interpretEmbeddedRun({ input, rules, outcome }): LintRun; // throws string
/// ```
pub fn interpret_embedded_run(
input: &[u8],
rules: &[MarkdownRule],
outcome: Result<EmbeddedRun, EmbeddedFailure>,
) -> Result<LintRun, String> {
let run: EmbeddedRun = match outcome {
Ok(usable) => usable,
Err(failure) if failure.exit_status == INCOMPLETE_STATUS => {
return Err(format!(
"monochromatic-lint could not finish (exit status {INCOMPLETE_STATUS}): {}",
explanation(failure.explanation.as_str())
));
}
Err(failure) => {
return Err(format!(
"monochromatic-lint exited with status {}, which its one-rule configuration never produces: {}",
failure.exit_status,
explanation(failure.explanation.as_str())
));
}
};
if run.fixed.len() > MAX_FIXED_BYTES {
return Err(format!(
"monochromatic-lint produced a fixed source of {} bytes, over the {MAX_FIXED_BYTES} bytes cli-git accepts",
run.fixed.len()
));
}
let findings_bytes: usize = run.findings.iter().fold(0, add_finding_bytes);
if findings_bytes > MAX_FINDINGS_BYTES {
return Err(format!(
"monochromatic-lint produced findings of {findings_bytes} bytes, over the {MAX_FINDINGS_BYTES} bytes cli-git accepts"
));
}
if run.fixed.is_empty() && !input.is_empty() {
return Err(String::from(
"monochromatic-lint produced an empty fixed source for a file that is not empty",
));
}
let mut remaining: Vec<RemainingFinding> = Vec::new();
for (index, finding) in run.findings.iter().enumerate() {
match remaining_finding(finding, rules) {
Ok(found) => remaining.push(found),
Err(reason) => {
return Err(format!(
"monochromatic-lint returned finding {} that cli-git cannot use: {reason}",
index + 1
));
}
}
}
return Ok(LintRun {
fixed: run.fixed.into_bytes(),
remaining,
});
}
/// Contract and explanation controls stay out of the release executable.
#[cfg(test)]
#[path = "markdown_linter_output_tests.rs"]
mod tests;