Repository navigation
Expand file tree
/
Copy patheslint_json.rs
More file actions
181 lines (177 loc) · 6.51 KB
/
Copy patheslint_json.rs
File metadata and controls
181 lines (177 loc) · 6.51 KB
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
//! ESLint 10 JSON 报告的有界局部观察;不执行配置、安装工具或签发门禁。
use crate::eslint_file_report::EslintFileReport;
use crate::eslint_message_report::EslintMessageReport;
use crate::{EslintDiagnostic, EslintParsed};
use std::collections::BTreeSet;
use std::path::{Component, Path};
/// 核对原生 JSON、具体版本、冻结文件集合及普通退出/max-warnings 参数。
/// 返回局部解析;解析器与抑制诊断保留未完成,未知输入不能成为干净范围。
#[must_use]
pub fn parse_eslint_json(
bytes: &[u8],
expected_version: &str,
observed_version: &str,
expected_files: &[String],
exit_code: Option<i32>,
max_warnings: Option<u64>,
) -> EslintParsed {
parse_inner(
bytes,
expected_version,
observed_version,
expected_files,
exit_code,
max_warnings,
)
.unwrap_or_else(|reason| EslintParsed {
local_coherent: false,
findings: vec![],
suppressed_count: 0,
reason: Some(reason),
})
}
fn parse_inner(
bytes: &[u8],
expected_version: &str,
observed_version: &str,
expected_files: &[String],
exit_code: Option<i32>,
max_warnings: Option<u64>,
) -> Result<EslintParsed, &'static str> {
if !eslint_report_version_matches(expected_version, observed_version) {
return Err("eslint_version_unverified");
}
if !matches!(exit_code, Some(0 | 1)) {
return Err("eslint_execution_incomplete");
}
if bytes.is_empty() || bytes.len() > 16 * 1024 * 1024 {
return Err("eslint_report_size_invalid");
}
let expected: BTreeSet<_> = expected_files.iter().map(String::as_str).collect();
if expected.is_empty()
|| expected.len() != expected_files.len()
|| expected.len() > 10_000
|| expected.iter().any(|p| !valid_path(p))
{
return Err("eslint_expected_scope_invalid");
}
let files: Vec<EslintFileReport> =
serde_json::from_slice(bytes).map_err(|_| "eslint_report_invalid")?;
let mut observed = BTreeSet::new();
let mut findings = Vec::new();
let (mut errors, mut warnings, mut suppressed_count, mut message_count) =
(0_u64, 0_u64, 0_u64, 0_usize);
let mut investigation = None;
for file in files {
if !valid_path(&file.file_path)
|| !expected.contains(file.file_path.as_str())
|| !observed.insert(file.file_path.clone())
{
return Err("eslint_report_scope_mismatch");
}
message_count = message_count
.checked_add(file.messages.len())
.and_then(|n| n.checked_add(file.suppressed_messages.len()))
.ok_or("eslint_report_size_invalid")?;
if message_count > 100_000 {
return Err("eslint_report_size_invalid");
}
let (mut file_errors, mut file_warnings, mut file_fatal) = (0_u64, 0_u64, 0_u64);
for message in file.messages {
validate_message(&message)?;
if message.severity == 2 {
file_errors += 1;
} else {
file_warnings += 1;
}
if message.fatal {
if message.severity != 2 {
return Err("eslint_report_counts_invalid");
}
file_fatal += 1;
investigation = Some("eslint_parser_or_configuration_diagnostic");
} else if let Some(rule) = message.rule_id.filter(|s| !s.is_empty()) {
let (Some(line), Some(column)) = (message.line, message.column) else {
return Err("eslint_rule_location_invalid");
};
if line == 0 || column == 0 {
return Err("eslint_rule_location_invalid");
}
findings.push(EslintDiagnostic {
path: file.file_path.clone(),
rule_id: rule,
severity: message.severity,
line,
column,
message: message.message,
});
} else if investigation.is_none() {
investigation = Some("eslint_unattributed_diagnostic");
}
}
if (file_errors, file_warnings, file_fatal)
!= (file.error_count, file.warning_count, file.fatal_error_count)
|| file.fixable_error_count > file_errors
|| file.fixable_warning_count > file_warnings
{
return Err("eslint_report_counts_invalid");
}
errors += file_errors;
warnings += file_warnings;
for suppressed in &file.suppressed_messages {
validate_message(suppressed)?;
}
suppressed_count += file.suppressed_messages.len() as u64;
}
if observed.iter().map(String::as_str).collect::<BTreeSet<_>>() != expected {
return Err("eslint_report_scope_mismatch");
}
let expected_exit = i32::from(errors > 0 || max_warnings.is_some_and(|max| warnings > max));
if exit_code != Some(expected_exit) {
return Err("eslint_exit_report_conflict");
}
let reason =
investigation.or((suppressed_count > 0).then_some("eslint_suppression_requires_review"));
Ok(EslintParsed {
local_coherent: reason.is_none(),
findings,
suppressed_count,
reason,
})
}
/// 核对具体稳定 ESLint 10 版本字符串;相同声明不证明工具身份或原生兼容。
#[must_use]
pub fn eslint_report_version_matches(expected: &str, observed: &str) -> bool {
expected == observed
&& semver::Version::parse(expected).is_ok_and(|version| {
version.major == 10 && version.pre.is_empty() && version.build.is_empty()
})
}
fn valid_path(value: &str) -> bool {
let path = Path::new(value);
!value.chars().any(char::is_control)
&& !value
.split(std::path::MAIN_SEPARATOR)
.any(|part| matches!(part, "." | ".."))
&& path.is_absolute()
&& path.components().all(|c| {
matches!(
c,
Component::RootDir | Component::Prefix(_) | Component::Normal(_)
)
})
}
fn validate_message(message: &EslintMessageReport) -> Result<(), &'static str> {
if !matches!(message.severity, 1 | 2)
|| message.message.is_empty()
|| message.message.len() > 4096
|| message
.rule_id
.as_ref()
.is_some_and(|r| r.len() > 512 || r.chars().any(char::is_control))
{
Err("eslint_message_invalid")
} else {
Ok(())
}
}