@frozenproductions/niteo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +458 -0
- package/Cargo.toml +13 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/niteo.js +51 -0
- package/package.json +27 -0
- package/scripts/build-binary.js +23 -0
- package/src/app.rs +132 -0
- package/src/cli.rs +43 -0
- package/src/config.rs +989 -0
- package/src/discovery.rs +42 -0
- package/src/git.rs +63 -0
- package/src/main.rs +13 -0
- package/src/report.rs +490 -0
- package/src/rules/max_directory_depth.rs +125 -0
- package/src/rules/max_file_exports.rs +478 -0
- package/src/rules/max_items_per_directory.rs +145 -0
- package/src/rules/min_items_per_directory.rs +146 -0
- package/src/rules/no_barrel_files.rs +284 -0
- package/src/rules/no_comments.rs +222 -0
- package/src/rules/no_console.rs +242 -0
- package/src/rules/no_debugger.rs +209 -0
- package/src/rules/no_default_export.rs +241 -0
- package/src/rules/no_duplicate_file_names.rs +196 -0
- package/src/rules/no_empty_directories.rs +467 -0
- package/src/rules/no_empty_interface.rs +280 -0
- package/src/rules/no_enums.rs +208 -0
- package/src/rules/no_eval.rs +268 -0
- package/src/rules/no_export_star.rs +252 -0
- package/src/rules/no_inline_types.rs +367 -0
- package/src/rules/no_interface.rs +570 -0
- package/src/rules/no_large_file.rs +98 -0
- package/src/rules/no_logic_in_barrel.rs +346 -0
- package/src/rules/no_logic_in_domain.rs +987 -0
- package/src/rules/no_mutable_exports.rs +253 -0
- package/src/rules/no_upward_import.rs +427 -0
- package/src/rules/prefer_satisfies.rs +319 -0
- package/src/rules.rs +247 -0
package/src/discovery.rs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use ignore::WalkBuilder;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
|
|
5
|
+
use crate::config::GitignoreConfig;
|
|
6
|
+
|
|
7
|
+
pub fn discover_files(
|
|
8
|
+
root: &Path,
|
|
9
|
+
scope: Option<&Path>,
|
|
10
|
+
gitignore_config: &GitignoreConfig,
|
|
11
|
+
) -> Result<Vec<PathBuf>> {
|
|
12
|
+
if !root.exists() {
|
|
13
|
+
return Ok(Vec::new());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let mut builder = WalkBuilder::new(root);
|
|
17
|
+
builder.git_ignore(gitignore_config.enabled);
|
|
18
|
+
builder.hidden(false);
|
|
19
|
+
builder.follow_links(false);
|
|
20
|
+
|
|
21
|
+
if let Some(scope) = scope {
|
|
22
|
+
let scope = scope.to_path_buf();
|
|
23
|
+
builder.filter_entry(move |entry| entry.path().starts_with(&scope));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let mut files = Vec::new();
|
|
27
|
+
for entry in builder.build() {
|
|
28
|
+
let entry = entry?;
|
|
29
|
+
if entry.path().is_file() && matches_typescript_file(entry.path()) {
|
|
30
|
+
files.push(entry.path().to_path_buf());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Ok(files)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn matches_typescript_file(path: &Path) -> bool {
|
|
38
|
+
matches!(
|
|
39
|
+
path.extension().and_then(|ext| ext.to_str()),
|
|
40
|
+
Some("ts") | Some("tsx")
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/git.rs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
use std::io::{self, Write};
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
pub fn get_changed_typescript_files() -> Vec<PathBuf> {
|
|
6
|
+
let output = Command::new("git")
|
|
7
|
+
.args(["diff", "--name-only", "HEAD"])
|
|
8
|
+
.output();
|
|
9
|
+
|
|
10
|
+
let staged_output = Command::new("git")
|
|
11
|
+
.args(["diff", "--name-only", "--cached"])
|
|
12
|
+
.output();
|
|
13
|
+
|
|
14
|
+
let mut files: Vec<PathBuf> = Vec::new();
|
|
15
|
+
|
|
16
|
+
if let Ok(output) = output {
|
|
17
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
18
|
+
for line in stdout.lines() {
|
|
19
|
+
if is_typescript_file(line) {
|
|
20
|
+
files.push(PathBuf::from(line));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if let Ok(output) = staged_output {
|
|
26
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
27
|
+
for line in stdout.lines() {
|
|
28
|
+
if is_typescript_file(line) {
|
|
29
|
+
let path = PathBuf::from(line);
|
|
30
|
+
if !files.contains(&path) {
|
|
31
|
+
files.push(path);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
files
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn prompt_scan_changed_files(changed_files: &[PathBuf]) -> bool {
|
|
41
|
+
if changed_files.is_empty() {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
println!("Found {} changed TypeScript file(s):", changed_files.len());
|
|
46
|
+
for file in changed_files {
|
|
47
|
+
println!(" {}", file.display());
|
|
48
|
+
}
|
|
49
|
+
println!();
|
|
50
|
+
|
|
51
|
+
print!("Scan only changed files? [Y/n] ");
|
|
52
|
+
io::stdout().flush().ok();
|
|
53
|
+
|
|
54
|
+
let mut input = String::new();
|
|
55
|
+
io::stdin().read_line(&mut input).ok();
|
|
56
|
+
|
|
57
|
+
let input = input.trim().to_lowercase();
|
|
58
|
+
input.is_empty() || input == "y" || input == "yes"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn is_typescript_file(path: &str) -> bool {
|
|
62
|
+
path.ends_with(".ts") || path.ends_with(".tsx")
|
|
63
|
+
}
|
package/src/main.rs
ADDED
package/src/report.rs
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
use std::path::PathBuf;
|
|
2
|
+
|
|
3
|
+
use crate::config::Severity;
|
|
4
|
+
use crate::rules::Violation;
|
|
5
|
+
|
|
6
|
+
const RED: &str = "\x1b[31m";
|
|
7
|
+
const YELLOW: &str = "\x1b[33m";
|
|
8
|
+
const GREEN: &str = "\x1b[32m";
|
|
9
|
+
const CYAN: &str = "\x1b[36m";
|
|
10
|
+
const BLUE: &str = "\x1b[34m";
|
|
11
|
+
const BOLD: &str = "\x1b[1m";
|
|
12
|
+
const DIM: &str = "\x1b[2m";
|
|
13
|
+
const RESET: &str = "\x1b[0m";
|
|
14
|
+
const DEFAULT_MAX_RULE_GROUPS: usize = 6;
|
|
15
|
+
const DEFAULT_MAX_FILES_PER_RULE: usize = 6;
|
|
16
|
+
const DEFAULT_MAX_LINES_PER_FILE: usize = 8;
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct Report {
|
|
20
|
+
files: Vec<PathBuf>,
|
|
21
|
+
violations: Vec<Violation>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Report {
|
|
25
|
+
pub fn new(files: Vec<PathBuf>, violations: Vec<Violation>) -> Self {
|
|
26
|
+
Self { files, violations }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn render_text(&self, verbose: bool) -> String {
|
|
30
|
+
let warning_count = self.count_by_severity(Severity::Warn);
|
|
31
|
+
let error_count = self.count_by_severity(Severity::Error);
|
|
32
|
+
let info_count = self.count_by_severity(Severity::Info);
|
|
33
|
+
let score = self.score(error_count, warning_count);
|
|
34
|
+
let mut output = String::new();
|
|
35
|
+
|
|
36
|
+
output.push_str(&render_header());
|
|
37
|
+
|
|
38
|
+
if self.violations.is_empty() {
|
|
39
|
+
output.push_str(&format!(
|
|
40
|
+
"{GREEN}{BOLD}No structural issues found.{RESET}\n"
|
|
41
|
+
));
|
|
42
|
+
} else {
|
|
43
|
+
let rule_groups = group_by_rule(&self.violations);
|
|
44
|
+
output.push_str(&render_findings(&rule_groups, verbose));
|
|
45
|
+
output.push('\n');
|
|
46
|
+
output.push_str(&render_end_summary(
|
|
47
|
+
self.files.len(),
|
|
48
|
+
self.violations.len(),
|
|
49
|
+
error_count,
|
|
50
|
+
warning_count,
|
|
51
|
+
info_count,
|
|
52
|
+
score,
|
|
53
|
+
&rule_groups,
|
|
54
|
+
verbose,
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
output
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn count_by_severity(&self, severity: Severity) -> usize {
|
|
62
|
+
self.violations
|
|
63
|
+
.iter()
|
|
64
|
+
.filter(|violation| violation.severity == severity)
|
|
65
|
+
.count()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn score(&self, error_count: usize, warning_count: usize) -> usize {
|
|
69
|
+
if self.files.is_empty() || self.violations.is_empty() {
|
|
70
|
+
return 100;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let weighted_findings = error_count.saturating_mul(2) + warning_count;
|
|
74
|
+
let files_scanned = self.files.len().max(1);
|
|
75
|
+
let penalty = weighted_findings.saturating_mul(100) / files_scanned;
|
|
76
|
+
|
|
77
|
+
100usize.saturating_sub(penalty)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[derive(Debug)]
|
|
82
|
+
struct RuleGroup<'a> {
|
|
83
|
+
severity: Severity,
|
|
84
|
+
rule: &'static str,
|
|
85
|
+
message: &'static str,
|
|
86
|
+
violations: Vec<&'a Violation>,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[derive(Debug)]
|
|
90
|
+
struct FileGroup<'a> {
|
|
91
|
+
file: PathBuf,
|
|
92
|
+
violations: Vec<&'a Violation>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn render_header() -> String {
|
|
96
|
+
format!("{BOLD}Niteo Structure Health{RESET}\n\n")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn render_end_summary(
|
|
100
|
+
file_count: usize,
|
|
101
|
+
violation_count: usize,
|
|
102
|
+
error_count: usize,
|
|
103
|
+
warning_count: usize,
|
|
104
|
+
info_count: usize,
|
|
105
|
+
score: usize,
|
|
106
|
+
rule_groups: &[RuleGroup<'_>],
|
|
107
|
+
verbose: bool,
|
|
108
|
+
) -> String {
|
|
109
|
+
let status = status_label(error_count, warning_count, info_count);
|
|
110
|
+
let status_color = status_color(error_count, warning_count, info_count);
|
|
111
|
+
let score_color = score_color(score);
|
|
112
|
+
let mut output = format!(
|
|
113
|
+
"{score_color}{BOLD}Score {score}/100{RESET} {status_color}{BOLD}{status}{RESET}\n\
|
|
114
|
+
{DIM}{file_count} files scanned | {violation_count} findings | {error_count} errors | {warning_count} warnings | {info_count} info{RESET}\n\n"
|
|
115
|
+
);
|
|
116
|
+
output.push_str(&render_rule_overview(rule_groups, verbose));
|
|
117
|
+
output
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn render_rule_overview(rule_groups: &[RuleGroup<'_>], verbose: bool) -> String {
|
|
121
|
+
let visible_count = visible_rule_group_count(rule_groups.len(), verbose);
|
|
122
|
+
let hidden_count = rule_groups.len().saturating_sub(visible_count);
|
|
123
|
+
let max_count_width = rule_groups
|
|
124
|
+
.iter()
|
|
125
|
+
.take(visible_count)
|
|
126
|
+
.map(|g| g.violations.len().to_string().len())
|
|
127
|
+
.max()
|
|
128
|
+
.unwrap_or(1);
|
|
129
|
+
let max_rule_width = rule_groups
|
|
130
|
+
.iter()
|
|
131
|
+
.take(visible_count)
|
|
132
|
+
.map(|g| g.rule.len())
|
|
133
|
+
.max()
|
|
134
|
+
.unwrap_or(1);
|
|
135
|
+
let mut output = format!("{BOLD}Rule Overview{RESET}\n");
|
|
136
|
+
|
|
137
|
+
for (index, group) in rule_groups.iter().take(visible_count).enumerate() {
|
|
138
|
+
let rank = index + 1;
|
|
139
|
+
let color = severity_color(group.severity);
|
|
140
|
+
let label = pluralized_label(group.severity);
|
|
141
|
+
let count_str = format!("{:>cw$}", group.violations.len(), cw = max_count_width);
|
|
142
|
+
let rule_str = format!("{:<rw$}", group.rule, rw = max_rule_width);
|
|
143
|
+
output.push_str(&format!(
|
|
144
|
+
" {DIM}{rank:>2}.{RESET} {color}{count_str}{RESET} {label} \
|
|
145
|
+
{rule_str} {message}\n",
|
|
146
|
+
message = group.message,
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if hidden_count > 0 {
|
|
151
|
+
output.push_str(&format!(
|
|
152
|
+
" {DIM}... {hidden_count} more rules hidden. Run with --verbose to show all.{RESET}\n"
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
output
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn render_findings(rule_groups: &[RuleGroup<'_>], verbose: bool) -> String {
|
|
160
|
+
let visible_count = visible_rule_group_count(rule_groups.len(), verbose);
|
|
161
|
+
let hidden_count = rule_groups.len().saturating_sub(visible_count);
|
|
162
|
+
let mut output = format!("{BOLD}Findings{RESET}\n");
|
|
163
|
+
|
|
164
|
+
for group in rule_groups.iter().take(visible_count) {
|
|
165
|
+
output.push_str(&render_rule_group(group, verbose));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if hidden_count > 0 {
|
|
169
|
+
output.push_str(&format!(
|
|
170
|
+
"{DIM}Hidden rule groups: {hidden_count}. Run with --verbose for the full report.{RESET}\n"
|
|
171
|
+
));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
output
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn render_rule_group(group: &RuleGroup<'_>, verbose: bool) -> String {
|
|
178
|
+
let color = severity_color(group.severity);
|
|
179
|
+
let label = pluralized_label(group.severity);
|
|
180
|
+
let file_groups = group_by_file(&group.violations);
|
|
181
|
+
let visible_file_count = visible_file_count(file_groups.len(), verbose);
|
|
182
|
+
let mut output = format!(
|
|
183
|
+
"\n{color}{BOLD}{rule}{RESET} {DIM}{count} {label} in {file_count} files{RESET}\n\
|
|
184
|
+
{DIM}{message}{RESET}\n",
|
|
185
|
+
rule = group.rule,
|
|
186
|
+
count = group.violations.len(),
|
|
187
|
+
file_count = file_groups.len(),
|
|
188
|
+
message = group.message,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
for file_group in file_groups.iter().take(visible_file_count) {
|
|
192
|
+
let line_count = visible_line_count(file_group.violations.len(), verbose);
|
|
193
|
+
let has_details = file_group
|
|
194
|
+
.violations
|
|
195
|
+
.iter()
|
|
196
|
+
.any(|v| v.detail.is_some() || v.subject.is_some());
|
|
197
|
+
|
|
198
|
+
if has_details {
|
|
199
|
+
let visible_violations = file_group.violations.iter().take(line_count);
|
|
200
|
+
for violation in visible_violations {
|
|
201
|
+
let subject = violation
|
|
202
|
+
.subject
|
|
203
|
+
.as_ref()
|
|
204
|
+
.map(|s| format!("{BOLD}{s}{RESET} "))
|
|
205
|
+
.unwrap_or_default();
|
|
206
|
+
let detail = violation
|
|
207
|
+
.detail
|
|
208
|
+
.as_ref()
|
|
209
|
+
.map(|d| format!(" {DIM}{d}{RESET}"))
|
|
210
|
+
.unwrap_or_default();
|
|
211
|
+
let location = match (violation.line, violation.column) {
|
|
212
|
+
(Some(line), Some(column)) => format!("lines {line}:{column}"),
|
|
213
|
+
(Some(line), None) => format!("line {line}"),
|
|
214
|
+
_ => String::new(),
|
|
215
|
+
};
|
|
216
|
+
let location_suffix = if location.is_empty() {
|
|
217
|
+
String::new()
|
|
218
|
+
} else {
|
|
219
|
+
format!(" {location}")
|
|
220
|
+
};
|
|
221
|
+
output.push_str(&format!(
|
|
222
|
+
" {CYAN}{}{RESET} {subject}{location_suffix}{detail}\n",
|
|
223
|
+
file_group.file.display(),
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
output.push_str(&format!(
|
|
228
|
+
" {CYAN}{}{RESET} {DIM}({} findings){RESET} {}\n",
|
|
229
|
+
file_group.file.display(),
|
|
230
|
+
file_group.violations.len(),
|
|
231
|
+
render_line_numbers(
|
|
232
|
+
file_group.violations.iter().take(line_count).copied(),
|
|
233
|
+
file_group.violations.len(),
|
|
234
|
+
verbose,
|
|
235
|
+
),
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let hidden_lines = file_group.violations.len().saturating_sub(line_count);
|
|
240
|
+
if hidden_lines > 0 && !has_details {
|
|
241
|
+
output.push_str(&format!(
|
|
242
|
+
" {DIM}+ {hidden_lines} more locations in this file. Use --verbose to show all.{RESET}\n"
|
|
243
|
+
));
|
|
244
|
+
} else if hidden_lines > 0 {
|
|
245
|
+
output.push_str(&format!(
|
|
246
|
+
" {DIM}+ {hidden_lines} more findings in this file. Use --verbose to show all.{RESET}\n"
|
|
247
|
+
));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let hidden_file_count = file_groups.len().saturating_sub(visible_file_count);
|
|
252
|
+
if hidden_file_count > 0 {
|
|
253
|
+
let hidden_violation_count = file_groups
|
|
254
|
+
.iter()
|
|
255
|
+
.skip(visible_file_count)
|
|
256
|
+
.map(|file_group| file_group.violations.len())
|
|
257
|
+
.sum::<usize>();
|
|
258
|
+
output.push_str(&format!(
|
|
259
|
+
" {DIM}+ {hidden_file_count} more files with {hidden_violation_count} findings. Use --verbose to show all files.{RESET}\n"
|
|
260
|
+
));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
output
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn group_by_rule<'a>(violations: &'a [Violation]) -> Vec<RuleGroup<'a>> {
|
|
267
|
+
let mut groups: Vec<RuleGroup<'a>> = Vec::new();
|
|
268
|
+
|
|
269
|
+
for violation in violations {
|
|
270
|
+
if let Some(group) = groups
|
|
271
|
+
.iter_mut()
|
|
272
|
+
.find(|group| group.severity == violation.severity && group.rule == violation.rule)
|
|
273
|
+
{
|
|
274
|
+
group.violations.push(violation);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
groups.push(RuleGroup {
|
|
279
|
+
severity: violation.severity,
|
|
280
|
+
rule: violation.rule,
|
|
281
|
+
message: violation.message,
|
|
282
|
+
violations: vec![violation],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for group in &mut groups {
|
|
287
|
+
group.violations.sort_by(|left, right| {
|
|
288
|
+
left.file
|
|
289
|
+
.cmp(&right.file)
|
|
290
|
+
.then(left.line.cmp(&right.line))
|
|
291
|
+
.then(left.column.cmp(&right.column))
|
|
292
|
+
.then(left.rule.cmp(right.rule))
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
groups.sort_by(|left, right| {
|
|
297
|
+
severity_rank(left.severity)
|
|
298
|
+
.cmp(&severity_rank(right.severity))
|
|
299
|
+
.then(right.violations.len().cmp(&left.violations.len()))
|
|
300
|
+
.then(left.rule.cmp(right.rule))
|
|
301
|
+
});
|
|
302
|
+
groups
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn group_by_file<'a>(violations: &[&'a Violation]) -> Vec<FileGroup<'a>> {
|
|
306
|
+
let mut groups: Vec<FileGroup<'a>> = Vec::new();
|
|
307
|
+
|
|
308
|
+
for violation in violations {
|
|
309
|
+
if let Some(group) = groups.iter_mut().find(|group| group.file == violation.file) {
|
|
310
|
+
group.violations.push(violation);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
groups.push(FileGroup {
|
|
315
|
+
file: violation.file.clone(),
|
|
316
|
+
violations: vec![violation],
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
groups.sort_by(|left, right| left.file.cmp(&right.file));
|
|
321
|
+
groups
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn visible_rule_group_count(group_count: usize, verbose: bool) -> usize {
|
|
325
|
+
if verbose {
|
|
326
|
+
return group_count;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
group_count.min(DEFAULT_MAX_RULE_GROUPS)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fn visible_file_count(file_count: usize, verbose: bool) -> usize {
|
|
333
|
+
if verbose {
|
|
334
|
+
return file_count;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
file_count.min(DEFAULT_MAX_FILES_PER_RULE)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn visible_line_count(line_count: usize, verbose: bool) -> usize {
|
|
341
|
+
if verbose {
|
|
342
|
+
return line_count;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
line_count.min(DEFAULT_MAX_LINES_PER_FILE)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn render_line_numbers<'a>(
|
|
349
|
+
violations: impl Iterator<Item = &'a Violation>,
|
|
350
|
+
total_count: usize,
|
|
351
|
+
verbose: bool,
|
|
352
|
+
) -> String {
|
|
353
|
+
let violations: Vec<&'a Violation> = violations.collect();
|
|
354
|
+
let positioned: Vec<&'a Violation> = violations
|
|
355
|
+
.iter()
|
|
356
|
+
.filter(|v| v.line.is_some())
|
|
357
|
+
.copied()
|
|
358
|
+
.collect();
|
|
359
|
+
let ranges = if verbose {
|
|
360
|
+
positioned
|
|
361
|
+
.iter()
|
|
362
|
+
.filter_map(|v| {
|
|
363
|
+
v.line
|
|
364
|
+
.map(|line| format!("{}:{}", line, v.column.unwrap_or(1)))
|
|
365
|
+
})
|
|
366
|
+
.collect::<Vec<String>>()
|
|
367
|
+
} else {
|
|
368
|
+
group_line_ranges(&positioned)
|
|
369
|
+
};
|
|
370
|
+
let lines = ranges.join(", ");
|
|
371
|
+
|
|
372
|
+
let suffix = if !verbose && total_count > DEFAULT_MAX_LINES_PER_FILE {
|
|
373
|
+
format!(
|
|
374
|
+
", {DIM}...and {} more{RESET}",
|
|
375
|
+
total_count.saturating_sub(DEFAULT_MAX_LINES_PER_FILE)
|
|
376
|
+
)
|
|
377
|
+
} else {
|
|
378
|
+
String::new()
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
if lines.is_empty() {
|
|
382
|
+
format!("{DIM}{suffix}{RESET}").trim().to_string()
|
|
383
|
+
} else {
|
|
384
|
+
format!("{DIM}lines {lines}{suffix}{RESET}")
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fn group_line_ranges(violations: &[&Violation]) -> Vec<String> {
|
|
389
|
+
if violations.is_empty() {
|
|
390
|
+
return Vec::new();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let mut ranges: Vec<String> = Vec::new();
|
|
394
|
+
let mut start = violations[0].line.unwrap_or(1);
|
|
395
|
+
let mut end = violations[0].line.unwrap_or(1);
|
|
396
|
+
|
|
397
|
+
for violation in violations.iter().skip(1) {
|
|
398
|
+
let line = violation.line.unwrap_or(1);
|
|
399
|
+
if line == end + 1 {
|
|
400
|
+
end = line;
|
|
401
|
+
} else {
|
|
402
|
+
if start == end {
|
|
403
|
+
ranges.push(format!("{start}"));
|
|
404
|
+
} else {
|
|
405
|
+
ranges.push(format!("{start}-{end}"));
|
|
406
|
+
}
|
|
407
|
+
start = line;
|
|
408
|
+
end = line;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if start == end {
|
|
413
|
+
ranges.push(format!("{start}"));
|
|
414
|
+
} else {
|
|
415
|
+
ranges.push(format!("{start}-{end}"));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
ranges
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fn status_label(error_count: usize, warning_count: usize, info_count: usize) -> &'static str {
|
|
422
|
+
if error_count > 0 {
|
|
423
|
+
return "Needs attention";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if warning_count > 0 {
|
|
427
|
+
return "Review recommended";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if info_count > 0 {
|
|
431
|
+
return "Suggestions available";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
"Healthy"
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fn status_color(error_count: usize, warning_count: usize, info_count: usize) -> &'static str {
|
|
438
|
+
if error_count > 0 {
|
|
439
|
+
return RED;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if warning_count > 0 {
|
|
443
|
+
return YELLOW;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if info_count > 0 {
|
|
447
|
+
return BLUE;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
GREEN
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
fn severity_color(severity: Severity) -> &'static str {
|
|
454
|
+
match severity {
|
|
455
|
+
Severity::Error => RED,
|
|
456
|
+
Severity::Warn => YELLOW,
|
|
457
|
+
Severity::Info => BLUE,
|
|
458
|
+
Severity::Off => DIM,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fn severity_rank(severity: Severity) -> usize {
|
|
463
|
+
match severity {
|
|
464
|
+
Severity::Error => 0,
|
|
465
|
+
Severity::Warn => 1,
|
|
466
|
+
Severity::Info => 2,
|
|
467
|
+
Severity::Off => 3,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fn pluralized_label(severity: Severity) -> &'static str {
|
|
472
|
+
match severity {
|
|
473
|
+
Severity::Error => "errors",
|
|
474
|
+
Severity::Warn => "warnings",
|
|
475
|
+
Severity::Info => "suggestions",
|
|
476
|
+
Severity::Off => "off",
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
fn score_color(score: usize) -> &'static str {
|
|
481
|
+
if score >= 75 {
|
|
482
|
+
return GREEN;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if score >= 50 {
|
|
486
|
+
return YELLOW;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
RED
|
|
490
|
+
}
|