sonicop 26.8.101
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.
- checksums.yaml +7 -0
- data/CONFORMANCE.md +109 -0
- data/Cargo.lock +975 -0
- data/Cargo.toml +40 -0
- data/LICENSE +21 -0
- data/NOTICE +7 -0
- data/README.ja.md +161 -0
- data/README.md +173 -0
- data/config/default.yml +6305 -0
- data/exe/sonicop +7 -0
- data/ext/sonicop/extconf.rb +38 -0
- data/lib/sonicop/runner.rb +67 -0
- data/lib/sonicop/version.rb +6 -0
- data/lib/sonicop.rb +8 -0
- data/licenses/RUBOCOP.txt +20 -0
- data/licenses/TREE_SITTER_RUBY.txt +21 -0
- data/src/cli.rs +940 -0
- data/src/config/inheritance.rs +332 -0
- data/src/config/loader.rs +117 -0
- data/src/config/mod.rs +461 -0
- data/src/config/paths.rs +436 -0
- data/src/config/plugin.rs +247 -0
- data/src/config/store.rs +141 -0
- data/src/cop_name.rs +71 -0
- data/src/diagnostic.rs +205 -0
- data/src/directives.rs +305 -0
- data/src/engine.rs +734 -0
- data/src/formatter.rs +684 -0
- data/src/lib.rs +42 -0
- data/src/main.rs +3 -0
- data/src/ruby_version.rs +393 -0
- data/src/rules/layout/empty_line_after_magic_comment.rs +49 -0
- data/src/rules/layout/end_of_line.rs +35 -0
- data/src/rules/layout/line_length.rs +167 -0
- data/src/rules/layout/mod.rs +13 -0
- data/src/rules/layout/space_after_comma.rs +32 -0
- data/src/rules/layout/space_around_operators.rs +104 -0
- data/src/rules/layout/space_inside_parens.rs +100 -0
- data/src/rules/layout/support.rs +23 -0
- data/src/rules/layout/trailing_empty_lines.rs +38 -0
- data/src/rules/layout/trailing_whitespace.rs +26 -0
- data/src/rules/lint/duplicate_methods.rs +72 -0
- data/src/rules/lint/mod.rs +7 -0
- data/src/rules/lint/syntax.rs +200 -0
- data/src/rules/lint/unused_block_argument.rs +74 -0
- data/src/rules/lint/useless_assignment.rs +104 -0
- data/src/rules/metrics/block_length.rs +39 -0
- data/src/rules/metrics/class_length.rs +34 -0
- data/src/rules/metrics/method_length.rs +10 -0
- data/src/rules/metrics/mod.rs +10 -0
- data/src/rules/metrics/module_length.rs +17 -0
- data/src/rules/metrics/parameter_lists.rs +21 -0
- data/src/rules/metrics/support.rs +105 -0
- data/src/rules/mod.rs +380 -0
- data/src/rules/naming/ascii_identifiers.rs +17 -0
- data/src/rules/naming/constant_name.rs +49 -0
- data/src/rules/naming/method_name.rs +51 -0
- data/src/rules/naming/mod.rs +9 -0
- data/src/rules/naming/support.rs +22 -0
- data/src/rules/naming/variable_name.rs +50 -0
- data/src/rules/security/eval.rs +170 -0
- data/src/rules/security/mod.rs +6 -0
- data/src/rules/style/frozen_string_literal_comment.rs +56 -0
- data/src/rules/style/hash_syntax.rs +63 -0
- data/src/rules/style/mod.rs +9 -0
- data/src/rules/style/numeric_literals.rs +59 -0
- data/src/rules/style/redundant_return.rs +143 -0
- data/src/rules/style/semicolon.rs +48 -0
- data/src/rules/style/string_literals.rs +74 -0
- data/src/rules/support.rs +31 -0
- data/src/source.rs +118 -0
- metadata +117 -0
data/src/cli.rs
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::io::{self, IsTerminal, Read, Write};
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
use std::time::Instant;
|
|
6
|
+
|
|
7
|
+
use anyhow::{Context, Result, bail};
|
|
8
|
+
use clap::{ArgAction, ArgMatches, CommandFactory, FromArgMatches, Parser, error::ErrorKind};
|
|
9
|
+
|
|
10
|
+
use crate::config::{Config, ConfigStore};
|
|
11
|
+
use crate::cop_name::{self, selector_matches};
|
|
12
|
+
use crate::diagnostic::{FileReport, Offense, Severity};
|
|
13
|
+
use crate::engine::{
|
|
14
|
+
CorrectMode, Selection, correct_file, discover_targets_with_store, inspect_files_with_store,
|
|
15
|
+
inspect_source, is_mandatory_cop, offense_count, write_corrected,
|
|
16
|
+
};
|
|
17
|
+
use crate::formatter::{
|
|
18
|
+
Format, FormatOptions, offenses_by_cop, render, smart_path, yaml_single_quoted,
|
|
19
|
+
};
|
|
20
|
+
use crate::rules::rule_names;
|
|
21
|
+
use crate::{RUBOCOP_COMPAT_VERSION, VERSION};
|
|
22
|
+
|
|
23
|
+
#[derive(Debug, Parser)]
|
|
24
|
+
#[command(name = "sonicop")]
|
|
25
|
+
#[command(about = "A fast, native RuboCop-compatible Ruby linter and formatter")]
|
|
26
|
+
#[command(disable_version_flag = true)]
|
|
27
|
+
#[command(max_term_width = 100)]
|
|
28
|
+
struct Cli {
|
|
29
|
+
/// Files or directories to inspect
|
|
30
|
+
#[arg(value_name = "FILE")]
|
|
31
|
+
paths: Vec<PathBuf>,
|
|
32
|
+
|
|
33
|
+
#[arg(short = 'l', long, help_heading = "Basic Options")]
|
|
34
|
+
lint: bool,
|
|
35
|
+
#[arg(short = 'x', long, help_heading = "Basic Options")]
|
|
36
|
+
fix_layout: bool,
|
|
37
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
38
|
+
safe: bool,
|
|
39
|
+
#[arg(long, value_name = "COP1,COP2", help_heading = "Basic Options")]
|
|
40
|
+
only: Option<String>,
|
|
41
|
+
#[arg(long, value_name = "COP1,COP2", help_heading = "Basic Options")]
|
|
42
|
+
except: Option<String>,
|
|
43
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
44
|
+
only_guide_cops: bool,
|
|
45
|
+
#[arg(short = 'F', long, help_heading = "Basic Options")]
|
|
46
|
+
fail_fast: bool,
|
|
47
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
48
|
+
disable_pending_cops: bool,
|
|
49
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
50
|
+
enable_pending_cops: bool,
|
|
51
|
+
#[arg(
|
|
52
|
+
long,
|
|
53
|
+
conflicts_with = "disable_all_cops",
|
|
54
|
+
help_heading = "Basic Options"
|
|
55
|
+
)]
|
|
56
|
+
enable_all_cops: bool,
|
|
57
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
58
|
+
disable_all_cops: bool,
|
|
59
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
60
|
+
ignore_disable_comments: bool,
|
|
61
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
62
|
+
force_exclusion: bool,
|
|
63
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
64
|
+
only_recognized_file_types: bool,
|
|
65
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
66
|
+
ignore_parent_exclusion: bool,
|
|
67
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
68
|
+
ignore_unrecognized_cops: bool,
|
|
69
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
70
|
+
force_default_config: bool,
|
|
71
|
+
#[arg(short = 's', long, value_name = "FILE", help_heading = "Basic Options")]
|
|
72
|
+
stdin: Option<PathBuf>,
|
|
73
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
74
|
+
editor_mode: bool,
|
|
75
|
+
#[arg(short = 'P', long, action = ArgAction::SetTrue, help_heading = "Basic Options")]
|
|
76
|
+
parallel: bool,
|
|
77
|
+
#[arg(long, conflicts_with = "parallel", help_heading = "Basic Options")]
|
|
78
|
+
no_parallel: bool,
|
|
79
|
+
#[arg(long, help_heading = "Basic Options")]
|
|
80
|
+
raise_cop_error: bool,
|
|
81
|
+
#[arg(
|
|
82
|
+
long,
|
|
83
|
+
default_value = "refactor",
|
|
84
|
+
value_name = "SEVERITY",
|
|
85
|
+
help_heading = "Basic Options"
|
|
86
|
+
)]
|
|
87
|
+
fail_level: String,
|
|
88
|
+
|
|
89
|
+
#[arg(short = 'C', long, value_name = "FLAG", help_heading = "Caching")]
|
|
90
|
+
cache: Option<String>,
|
|
91
|
+
#[arg(long, value_name = "DIR", help_heading = "Caching")]
|
|
92
|
+
cache_root: Option<PathBuf>,
|
|
93
|
+
|
|
94
|
+
#[arg(short = 'f', long = "format", action = ArgAction::Append, value_name = "FORMATTER", help_heading = "Output Options")]
|
|
95
|
+
formats: Vec<String>,
|
|
96
|
+
#[arg(short = 'D', long, action = ArgAction::SetTrue, help_heading = "Output Options")]
|
|
97
|
+
display_cop_names: bool,
|
|
98
|
+
#[arg(long, help_heading = "Output Options")]
|
|
99
|
+
no_display_cop_names: bool,
|
|
100
|
+
#[arg(short = 'E', long, help_heading = "Output Options")]
|
|
101
|
+
extra_details: bool,
|
|
102
|
+
#[arg(short = 'S', long, help_heading = "Output Options")]
|
|
103
|
+
display_style_guide: bool,
|
|
104
|
+
#[arg(short = 'o', long, action = ArgAction::Append, value_name = "FILE", help_heading = "Output Options")]
|
|
105
|
+
out: Vec<PathBuf>,
|
|
106
|
+
#[arg(long, help_heading = "Output Options")]
|
|
107
|
+
stderr: bool,
|
|
108
|
+
#[arg(long, help_heading = "Output Options")]
|
|
109
|
+
display_time: bool,
|
|
110
|
+
#[arg(long, help_heading = "Output Options")]
|
|
111
|
+
display_only_failed: bool,
|
|
112
|
+
#[arg(long, help_heading = "Output Options")]
|
|
113
|
+
display_only_fail_level_offenses: bool,
|
|
114
|
+
#[arg(long, help_heading = "Output Options")]
|
|
115
|
+
display_only_correctable: bool,
|
|
116
|
+
#[arg(long, help_heading = "Output Options")]
|
|
117
|
+
display_only_safe_correctable: bool,
|
|
118
|
+
#[arg(long, help_heading = "Output Options")]
|
|
119
|
+
display_suppressed: bool,
|
|
120
|
+
|
|
121
|
+
#[arg(short = 'a', long = "autocorrect", help_heading = "Autocorrection")]
|
|
122
|
+
autocorrect: bool,
|
|
123
|
+
#[arg(short = 'A', long = "autocorrect-all", help_heading = "Autocorrection")]
|
|
124
|
+
autocorrect_all: bool,
|
|
125
|
+
#[arg(long = "auto-correct", help_heading = "Autocorrection")]
|
|
126
|
+
deprecated_auto_correct: bool,
|
|
127
|
+
#[arg(long = "safe-auto-correct", help_heading = "Autocorrection")]
|
|
128
|
+
deprecated_safe_auto_correct: bool,
|
|
129
|
+
#[arg(long = "auto-correct-all", help_heading = "Autocorrection")]
|
|
130
|
+
deprecated_auto_correct_all: bool,
|
|
131
|
+
#[arg(long, help_heading = "Autocorrection")]
|
|
132
|
+
disable_uncorrectable: bool,
|
|
133
|
+
|
|
134
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
135
|
+
auto_gen_config: bool,
|
|
136
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
137
|
+
regenerate_todo: bool,
|
|
138
|
+
#[arg(
|
|
139
|
+
long,
|
|
140
|
+
default_value_t = 15,
|
|
141
|
+
value_name = "COUNT",
|
|
142
|
+
help_heading = "Config Generation"
|
|
143
|
+
)]
|
|
144
|
+
exclude_limit: usize,
|
|
145
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
146
|
+
no_exclude_limit: bool,
|
|
147
|
+
#[arg(long, action = ArgAction::SetTrue, help_heading = "Config Generation")]
|
|
148
|
+
offense_counts: bool,
|
|
149
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
150
|
+
no_offense_counts: bool,
|
|
151
|
+
#[arg(long, action = ArgAction::SetTrue, help_heading = "Config Generation")]
|
|
152
|
+
auto_gen_only_exclude: bool,
|
|
153
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
154
|
+
no_auto_gen_only_exclude: bool,
|
|
155
|
+
#[arg(long, action = ArgAction::SetTrue, help_heading = "Config Generation")]
|
|
156
|
+
auto_gen_timestamp: bool,
|
|
157
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
158
|
+
no_auto_gen_timestamp: bool,
|
|
159
|
+
#[arg(long, action = ArgAction::SetTrue, help_heading = "Config Generation")]
|
|
160
|
+
auto_gen_enforced_style: bool,
|
|
161
|
+
#[arg(long, help_heading = "Config Generation")]
|
|
162
|
+
no_auto_gen_enforced_style: bool,
|
|
163
|
+
|
|
164
|
+
#[arg(long, help_heading = "LSP Option")]
|
|
165
|
+
lsp: bool,
|
|
166
|
+
#[arg(long, help_heading = "MCP Option")]
|
|
167
|
+
mcp: bool,
|
|
168
|
+
#[arg(long, help_heading = "Server Options")]
|
|
169
|
+
server: bool,
|
|
170
|
+
#[arg(long, help_heading = "Server Options")]
|
|
171
|
+
no_server: bool,
|
|
172
|
+
#[arg(long, help_heading = "Server Options")]
|
|
173
|
+
restart_server: bool,
|
|
174
|
+
#[arg(long, help_heading = "Server Options")]
|
|
175
|
+
start_server: bool,
|
|
176
|
+
#[arg(long, help_heading = "Server Options")]
|
|
177
|
+
stop_server: bool,
|
|
178
|
+
#[arg(long, help_heading = "Server Options")]
|
|
179
|
+
server_status: bool,
|
|
180
|
+
#[arg(long, help_heading = "Server Options")]
|
|
181
|
+
no_detach: bool,
|
|
182
|
+
|
|
183
|
+
#[arg(short = 'L', long, help_heading = "Additional Modes")]
|
|
184
|
+
list_target_files: bool,
|
|
185
|
+
#[arg(long, value_name = "PATH", help_heading = "Additional Modes")]
|
|
186
|
+
list_enabled_cops_for: Option<PathBuf>,
|
|
187
|
+
#[arg(long, num_args = 0..=1, default_missing_value = "", value_name = "COP1,COP2", help_heading = "Additional Modes")]
|
|
188
|
+
show_cops: Option<String>,
|
|
189
|
+
#[arg(long, num_args = 0..=1, default_missing_value = "", value_name = "COP1,COP2", help_heading = "Additional Modes")]
|
|
190
|
+
show_docs_url: Option<String>,
|
|
191
|
+
|
|
192
|
+
#[arg(long, help_heading = "General Options")]
|
|
193
|
+
init: bool,
|
|
194
|
+
#[arg(
|
|
195
|
+
short = 'c',
|
|
196
|
+
long,
|
|
197
|
+
value_name = "FILE",
|
|
198
|
+
help_heading = "General Options"
|
|
199
|
+
)]
|
|
200
|
+
config: Option<PathBuf>,
|
|
201
|
+
#[arg(short = 'd', long, help_heading = "General Options")]
|
|
202
|
+
debug: bool,
|
|
203
|
+
#[arg(long, action = ArgAction::Append, value_name = "FILE", help_heading = "General Options")]
|
|
204
|
+
plugin: Vec<String>,
|
|
205
|
+
#[arg(short = 'r', long = "require", action = ArgAction::Append, value_name = "FILE", help_heading = "General Options")]
|
|
206
|
+
requires: Vec<String>,
|
|
207
|
+
#[arg(long, action = ArgAction::SetTrue, help_heading = "General Options")]
|
|
208
|
+
color: bool,
|
|
209
|
+
#[arg(long, conflicts_with = "color", help_heading = "General Options")]
|
|
210
|
+
no_color: bool,
|
|
211
|
+
#[arg(short = 'v', long = "version", help_heading = "General Options")]
|
|
212
|
+
version: bool,
|
|
213
|
+
#[arg(
|
|
214
|
+
short = 'V',
|
|
215
|
+
long = "verbose-version",
|
|
216
|
+
help_heading = "General Options"
|
|
217
|
+
)]
|
|
218
|
+
verbose_version: bool,
|
|
219
|
+
#[arg(long, help_heading = "Profiling Options")]
|
|
220
|
+
profile: bool,
|
|
221
|
+
#[arg(long, help_heading = "Profiling Options")]
|
|
222
|
+
memory: bool,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
impl Cli {
|
|
226
|
+
fn correct_mode(&self) -> CorrectMode {
|
|
227
|
+
if self.autocorrect_all || self.deprecated_auto_correct_all || self.fix_layout {
|
|
228
|
+
CorrectMode::All
|
|
229
|
+
} else if self.autocorrect
|
|
230
|
+
|| self.deprecated_auto_correct
|
|
231
|
+
|| self.deprecated_safe_auto_correct
|
|
232
|
+
{
|
|
233
|
+
CorrectMode::Safe
|
|
234
|
+
} else {
|
|
235
|
+
CorrectMode::None
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pub fn run() -> i32 {
|
|
241
|
+
let arguments = match composed_arguments() {
|
|
242
|
+
Ok(arguments) => arguments,
|
|
243
|
+
Err(error) => {
|
|
244
|
+
eprintln!("Error: {error:#}");
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
let matches = match Cli::command().try_get_matches_from(arguments) {
|
|
249
|
+
Ok(matches) => matches,
|
|
250
|
+
Err(error)
|
|
251
|
+
if matches!(
|
|
252
|
+
error.kind(),
|
|
253
|
+
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
|
|
254
|
+
) =>
|
|
255
|
+
{
|
|
256
|
+
print!("{error}");
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
Err(error) => {
|
|
260
|
+
eprint!("{error}");
|
|
261
|
+
return 2;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
let cli = match Cli::from_arg_matches(&matches) {
|
|
265
|
+
Ok(cli) => cli,
|
|
266
|
+
Err(error) => {
|
|
267
|
+
eprint!("{error}");
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
let outputs = output_paths_by_format(&matches);
|
|
272
|
+
match try_run(cli, &outputs) {
|
|
273
|
+
Ok(code) => code,
|
|
274
|
+
Err(error) => {
|
|
275
|
+
eprintln!("Error: {error:#}");
|
|
276
|
+
2
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// RuboCop attaches each `--out` to the `--format` that preceded it on the command line, so the
|
|
282
|
+
/// slots have to be filled by argument position rather than by ordinal. An `--out` given before any
|
|
283
|
+
/// `--format` belongs to the default formatter, and a second `--out` on the same format is dropped.
|
|
284
|
+
fn output_paths_by_format(matches: &ArgMatches) -> Vec<Option<PathBuf>> {
|
|
285
|
+
let positions = |id| -> Vec<usize> {
|
|
286
|
+
matches
|
|
287
|
+
.indices_of(id)
|
|
288
|
+
.map(Iterator::collect)
|
|
289
|
+
.unwrap_or_default()
|
|
290
|
+
};
|
|
291
|
+
let format_positions = positions("formats");
|
|
292
|
+
let out_positions = positions("out");
|
|
293
|
+
let out_paths: Vec<&PathBuf> = matches
|
|
294
|
+
.get_many::<PathBuf>("out")
|
|
295
|
+
.map(Iterator::collect)
|
|
296
|
+
.unwrap_or_default();
|
|
297
|
+
|
|
298
|
+
let mut slots = vec![None; format_positions.len().max(1)];
|
|
299
|
+
for (out_position, path) in out_positions.into_iter().zip(out_paths) {
|
|
300
|
+
let slot = format_positions
|
|
301
|
+
.iter()
|
|
302
|
+
.rposition(|format_position| *format_position < out_position)
|
|
303
|
+
.unwrap_or(0);
|
|
304
|
+
slots[slot].get_or_insert_with(|| path.clone());
|
|
305
|
+
}
|
|
306
|
+
slots
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn composed_arguments() -> Result<Vec<String>> {
|
|
310
|
+
let mut arguments = vec![
|
|
311
|
+
std::env::args()
|
|
312
|
+
.next()
|
|
313
|
+
.unwrap_or_else(|| "sonicop".to_owned()),
|
|
314
|
+
];
|
|
315
|
+
let dotfile = std::env::current_dir()?.join(".rubocop");
|
|
316
|
+
if dotfile.is_file() {
|
|
317
|
+
let contents = fs::read_to_string(&dotfile)
|
|
318
|
+
.with_context(|| format!("failed to read {}", dotfile.display()))?;
|
|
319
|
+
arguments.extend(
|
|
320
|
+
shell_words::split(&contents)
|
|
321
|
+
.with_context(|| format!("failed to parse {}", dotfile.display()))?,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if let Some(options) = std::env::var_os("RUBOCOP_OPTS") {
|
|
325
|
+
let options = options
|
|
326
|
+
.into_string()
|
|
327
|
+
.map_err(|_| anyhow::anyhow!("RUBOCOP_OPTS is not valid UTF-8"))?;
|
|
328
|
+
arguments.extend(shell_words::split(&options).context("failed to parse RUBOCOP_OPTS")?);
|
|
329
|
+
}
|
|
330
|
+
arguments.extend(std::env::args().skip(1));
|
|
331
|
+
Ok(arguments)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fn try_run(cli: Cli, outputs: &[Option<PathBuf>]) -> Result<i32> {
|
|
335
|
+
let started = Instant::now();
|
|
336
|
+
if cli.version {
|
|
337
|
+
println!("{VERSION}");
|
|
338
|
+
return Ok(0);
|
|
339
|
+
}
|
|
340
|
+
if cli.verbose_version {
|
|
341
|
+
let cwd = std::env::current_dir().context("failed to determine current directory")?;
|
|
342
|
+
let config =
|
|
343
|
+
Config::load_with_options(cli.config.as_deref(), &cwd, cli.force_default_config)?;
|
|
344
|
+
println!(
|
|
345
|
+
"sonicop {VERSION} (RuboCop {RUBOCOP_COMPAT_VERSION} CLI, tree-sitter-ruby owayo@88a64c6, analyzing as Ruby {}) [{} {}]",
|
|
346
|
+
config.target_ruby_version(),
|
|
347
|
+
std::env::consts::OS,
|
|
348
|
+
std::env::consts::ARCH
|
|
349
|
+
);
|
|
350
|
+
return Ok(0);
|
|
351
|
+
}
|
|
352
|
+
validate_compatibility(&cli)?;
|
|
353
|
+
let fail_level = FailLevel::parse(&cli.fail_level)?;
|
|
354
|
+
print_deprecation_warnings(&cli);
|
|
355
|
+
|
|
356
|
+
let cwd = std::env::current_dir().context("failed to determine current directory")?;
|
|
357
|
+
if cli.init {
|
|
358
|
+
return init_config(&cwd);
|
|
359
|
+
}
|
|
360
|
+
let config = Config::load_with_options(cli.config.as_deref(), &cwd, cli.force_default_config)?;
|
|
361
|
+
validate_config(&config, cli.ignore_unrecognized_cops)?;
|
|
362
|
+
let configs = ConfigStore::new(
|
|
363
|
+
config.clone(),
|
|
364
|
+
cli.config.is_none() && !cli.force_default_config,
|
|
365
|
+
cli.ignore_unrecognized_cops,
|
|
366
|
+
);
|
|
367
|
+
report_noop_modes(&cli);
|
|
368
|
+
|
|
369
|
+
if cli.lsp || cli.mcp {
|
|
370
|
+
return Ok(0);
|
|
371
|
+
}
|
|
372
|
+
if let Some(filter) = &cli.show_cops {
|
|
373
|
+
show_cops(filter, &config);
|
|
374
|
+
return Ok(0);
|
|
375
|
+
}
|
|
376
|
+
if let Some(filter) = &cli.show_docs_url {
|
|
377
|
+
show_docs_urls(filter, &config);
|
|
378
|
+
return Ok(0);
|
|
379
|
+
}
|
|
380
|
+
if let Some(path) = &cli.list_enabled_cops_for {
|
|
381
|
+
list_enabled_cops(path, &configs)?;
|
|
382
|
+
return Ok(0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let mut only = csv(cli.only.as_deref());
|
|
386
|
+
if cli.lint {
|
|
387
|
+
only.push("Lint".to_owned());
|
|
388
|
+
}
|
|
389
|
+
if cli.fix_layout {
|
|
390
|
+
only.push("Layout".to_owned());
|
|
391
|
+
}
|
|
392
|
+
validate_selection(&only, "--only", &config)?;
|
|
393
|
+
let except = csv(cli.except.as_deref());
|
|
394
|
+
validate_selection(&except, "--except", &config)?;
|
|
395
|
+
let selection = Selection {
|
|
396
|
+
only,
|
|
397
|
+
except,
|
|
398
|
+
disable_all: cli.disable_all_cops,
|
|
399
|
+
enable_all: cli.enable_all_cops,
|
|
400
|
+
enable_pending: cli.enable_pending_cops,
|
|
401
|
+
disable_pending: cli.disable_pending_cops,
|
|
402
|
+
safe_only: cli.safe,
|
|
403
|
+
ignore_disable_comments: cli.ignore_disable_comments,
|
|
404
|
+
display_suppressed: cli.display_suppressed,
|
|
405
|
+
};
|
|
406
|
+
let correct_mode = cli.correct_mode();
|
|
407
|
+
|
|
408
|
+
let parallel = !cli.no_parallel && (cli.parallel || cli.stdin.is_none());
|
|
409
|
+
let mut reports = if let Some(stdin_path) = &cli.stdin {
|
|
410
|
+
if !cli.paths.is_empty() {
|
|
411
|
+
bail!(
|
|
412
|
+
"--stdin requires exactly one path supplied as its argument and no file arguments"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
let mut text = String::new();
|
|
416
|
+
io::stdin()
|
|
417
|
+
.read_to_string(&mut text)
|
|
418
|
+
.context("failed to read UTF-8 source from stdin")?;
|
|
419
|
+
let target_config = configs.for_path(stdin_path)?;
|
|
420
|
+
vec![inspect_source(
|
|
421
|
+
stdin_path.clone(),
|
|
422
|
+
text,
|
|
423
|
+
&target_config,
|
|
424
|
+
&selection,
|
|
425
|
+
)?]
|
|
426
|
+
} else {
|
|
427
|
+
let mut targets = discover_targets_with_store(
|
|
428
|
+
&cli.paths,
|
|
429
|
+
&cwd,
|
|
430
|
+
&configs,
|
|
431
|
+
cli.force_exclusion,
|
|
432
|
+
cli.only_recognized_file_types,
|
|
433
|
+
)?;
|
|
434
|
+
if cli.list_target_files {
|
|
435
|
+
for path in targets {
|
|
436
|
+
println!("{}", smart_path(&path, &cwd));
|
|
437
|
+
}
|
|
438
|
+
return Ok(0);
|
|
439
|
+
}
|
|
440
|
+
if cli.fail_fast {
|
|
441
|
+
targets.sort_by_key(|path| {
|
|
442
|
+
std::cmp::Reverse(
|
|
443
|
+
fs::metadata(path)
|
|
444
|
+
.and_then(|metadata| metadata.modified())
|
|
445
|
+
.ok(),
|
|
446
|
+
)
|
|
447
|
+
});
|
|
448
|
+
inspect_fail_fast(&targets, &configs, &selection)?
|
|
449
|
+
} else {
|
|
450
|
+
inspect_files_with_store(&targets, &configs, &selection, parallel)?
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
if cli.debug {
|
|
455
|
+
debug_report(&cwd, &config, reports.len(), parallel);
|
|
456
|
+
}
|
|
457
|
+
if cli.auto_gen_config || cli.regenerate_todo {
|
|
458
|
+
generate_todo(&reports, &cwd, &cli)?;
|
|
459
|
+
return Ok(0);
|
|
460
|
+
}
|
|
461
|
+
if cli.disable_uncorrectable {
|
|
462
|
+
eprintln!(
|
|
463
|
+
"Sonicop: --disable-uncorrectable is accepted; todo insertion is not implemented yet."
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let mut corrected_count = 0;
|
|
468
|
+
let mut stdin_corrected = None;
|
|
469
|
+
let mut run_errors = 0;
|
|
470
|
+
let mut corrected_reports = Vec::with_capacity(reports.len());
|
|
471
|
+
for report in reports {
|
|
472
|
+
let path = report.path.clone();
|
|
473
|
+
let target_config = configs.for_path(&path)?;
|
|
474
|
+
let outcome = correct_file(report, correct_mode, &target_config, &selection)?;
|
|
475
|
+
corrected_count += outcome.corrected_count;
|
|
476
|
+
if let Some(message) = outcome.infinite_loop {
|
|
477
|
+
// RuboCop keeps the run going and still writes what it managed to correct.
|
|
478
|
+
eprintln!("{message}");
|
|
479
|
+
run_errors += 1;
|
|
480
|
+
}
|
|
481
|
+
if outcome.corrected_count > 0 {
|
|
482
|
+
if cli.stdin.is_some() {
|
|
483
|
+
stdin_corrected = Some(outcome.text);
|
|
484
|
+
} else {
|
|
485
|
+
write_corrected(&path, &outcome.text)?;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
corrected_reports.push(outcome.report);
|
|
489
|
+
}
|
|
490
|
+
reports = corrected_reports;
|
|
491
|
+
|
|
492
|
+
// A file RuboCop could not finish counts as a failed run even when nothing else offended.
|
|
493
|
+
let failing = fail_level.failing(&reports) || run_errors > 0;
|
|
494
|
+
|
|
495
|
+
if let Some(corrected) = stdin_corrected {
|
|
496
|
+
print!("{corrected}");
|
|
497
|
+
} else {
|
|
498
|
+
filter_displayed_offenses(&mut reports, &cli, fail_level);
|
|
499
|
+
render_outputs(
|
|
500
|
+
&RenderRequest {
|
|
501
|
+
cli: &cli,
|
|
502
|
+
config: &config,
|
|
503
|
+
cwd: &cwd,
|
|
504
|
+
fail_level,
|
|
505
|
+
outputs,
|
|
506
|
+
corrected_count,
|
|
507
|
+
elapsed: started.elapsed().as_secs_f64(),
|
|
508
|
+
},
|
|
509
|
+
&reports,
|
|
510
|
+
)?;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
Ok(i32::from(failing))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/// RuboCop's `--fail-level` accepts a severity plus the pseudo level `autocorrect`, which changes
|
|
517
|
+
/// which offenses count as failures rather than raising the severity threshold.
|
|
518
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
519
|
+
enum FailLevel {
|
|
520
|
+
Severity(Severity),
|
|
521
|
+
Autocorrect,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
impl FailLevel {
|
|
525
|
+
fn parse(value: &str) -> Result<Self> {
|
|
526
|
+
if matches!(value.to_ascii_lowercase().as_str(), "a" | "autocorrect") {
|
|
527
|
+
return Ok(Self::Autocorrect);
|
|
528
|
+
}
|
|
529
|
+
Severity::parse(value)
|
|
530
|
+
.map(Self::Severity)
|
|
531
|
+
.with_context(|| format!("unknown severity: {value}"))
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// `autocorrect` is not a severity, so everything that needs a threshold falls back to the
|
|
535
|
+
/// default the way RuboCop's `minimum_severity_to_fail` does.
|
|
536
|
+
fn severity(self) -> Severity {
|
|
537
|
+
match self {
|
|
538
|
+
Self::Severity(severity) => severity,
|
|
539
|
+
Self::Autocorrect => Severity::Refactor,
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
fn failing(self, reports: &[FileReport]) -> bool {
|
|
544
|
+
match self {
|
|
545
|
+
// A correctable offense fails even once it has been corrected.
|
|
546
|
+
Self::Autocorrect => reports
|
|
547
|
+
.iter()
|
|
548
|
+
.flat_map(|report| &report.offenses)
|
|
549
|
+
.any(Offense::is_correctable),
|
|
550
|
+
Self::Severity(severity) => offense_count(reports, severity) > 0,
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fn validate_compatibility(cli: &Cli) -> Result<()> {
|
|
556
|
+
if cli.except.as_deref().is_some_and(|value| {
|
|
557
|
+
csv(Some(value))
|
|
558
|
+
.iter()
|
|
559
|
+
.any(|name| is_mandatory_cop(name.as_str()))
|
|
560
|
+
}) {
|
|
561
|
+
bail!("Syntax checking cannot be turned off.");
|
|
562
|
+
}
|
|
563
|
+
if let Some(cache) = &cli.cache
|
|
564
|
+
&& !matches!(cache.as_str(), "true" | "false")
|
|
565
|
+
{
|
|
566
|
+
bail!("-C/--cache argument must be true or false");
|
|
567
|
+
}
|
|
568
|
+
if cli.cache.as_deref() == Some("false") && cli.cache_root.is_some() {
|
|
569
|
+
bail!("--cache-root cannot be used with --cache false");
|
|
570
|
+
}
|
|
571
|
+
if cli.display_only_failed
|
|
572
|
+
&& !cli
|
|
573
|
+
.formats
|
|
574
|
+
.iter()
|
|
575
|
+
.any(|format| matches!(format.as_str(), "junit" | "ju"))
|
|
576
|
+
{
|
|
577
|
+
bail!("--display-only-failed can only be used with --format junit");
|
|
578
|
+
}
|
|
579
|
+
if cli.display_only_correctable && (cli.autocorrect || cli.autocorrect_all || cli.fix_layout) {
|
|
580
|
+
bail!("--display-only-correctable cannot be combined with autocorrection");
|
|
581
|
+
}
|
|
582
|
+
if cli.lsp && cli.editor_mode {
|
|
583
|
+
bail!("--lsp cannot be combined with --editor-mode");
|
|
584
|
+
}
|
|
585
|
+
Ok(())
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
fn print_deprecation_warnings(cli: &Cli) {
|
|
589
|
+
if cli.deprecated_auto_correct {
|
|
590
|
+
eprintln!("--auto-correct is deprecated; use --autocorrect instead.");
|
|
591
|
+
}
|
|
592
|
+
if cli.deprecated_safe_auto_correct {
|
|
593
|
+
eprintln!("--safe-auto-correct is deprecated; use --autocorrect instead.");
|
|
594
|
+
}
|
|
595
|
+
if cli.deprecated_auto_correct_all {
|
|
596
|
+
eprintln!("--auto-correct-all is deprecated; use --autocorrect-all instead.");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
fn report_noop_modes(cli: &Cli) {
|
|
601
|
+
if cli.server
|
|
602
|
+
|| cli.no_server
|
|
603
|
+
|| cli.restart_server
|
|
604
|
+
|| cli.start_server
|
|
605
|
+
|| cli.stop_server
|
|
606
|
+
|| cli.server_status
|
|
607
|
+
|| cli.no_detach
|
|
608
|
+
{
|
|
609
|
+
eprintln!(
|
|
610
|
+
"Sonicop: server flags are accepted as no-ops because native startup is immediate."
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if cli.lsp {
|
|
614
|
+
eprintln!("Sonicop: --lsp is accepted; the LSP transport is not implemented yet.");
|
|
615
|
+
}
|
|
616
|
+
if cli.mcp {
|
|
617
|
+
eprintln!("Sonicop: --mcp is accepted; the MCP transport is not implemented yet.");
|
|
618
|
+
}
|
|
619
|
+
if !cli.plugin.is_empty() || !cli.requires.is_empty() {
|
|
620
|
+
eprintln!("Sonicop: Ruby plugins and --require entries are accepted but not executed.");
|
|
621
|
+
}
|
|
622
|
+
if cli.profile || cli.memory {
|
|
623
|
+
eprintln!(
|
|
624
|
+
"Sonicop: profiling flags are accepted; use an external Rust profiler for native traces."
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
let _ = (
|
|
628
|
+
cli.only_guide_cops,
|
|
629
|
+
cli.ignore_parent_exclusion,
|
|
630
|
+
cli.raise_cop_error,
|
|
631
|
+
cli.offense_counts,
|
|
632
|
+
cli.no_offense_counts,
|
|
633
|
+
cli.auto_gen_only_exclude,
|
|
634
|
+
cli.no_auto_gen_only_exclude,
|
|
635
|
+
cli.auto_gen_timestamp,
|
|
636
|
+
cli.no_auto_gen_timestamp,
|
|
637
|
+
cli.auto_gen_enforced_style,
|
|
638
|
+
cli.no_auto_gen_enforced_style,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
fn inspect_fail_fast(
|
|
643
|
+
paths: &[PathBuf],
|
|
644
|
+
configs: &ConfigStore,
|
|
645
|
+
selection: &Selection,
|
|
646
|
+
) -> Result<Vec<FileReport>> {
|
|
647
|
+
let mut reports = Vec::new();
|
|
648
|
+
for path in paths {
|
|
649
|
+
let mut inspected =
|
|
650
|
+
inspect_files_with_store(std::slice::from_ref(path), configs, selection, false)?;
|
|
651
|
+
let has_offense = inspected
|
|
652
|
+
.first()
|
|
653
|
+
.is_some_and(|report| !report.offenses.is_empty());
|
|
654
|
+
reports.append(&mut inspected);
|
|
655
|
+
if has_offense {
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
Ok(reports)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
fn validate_config(config: &Config, ignore_unrecognized: bool) -> Result<()> {
|
|
663
|
+
if !ignore_unrecognized && !config.unrecognized_cop_names().is_empty() {
|
|
664
|
+
bail!(
|
|
665
|
+
"unrecognized cop(s): {}",
|
|
666
|
+
config.unrecognized_cop_names().join(", ")
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
Ok(())
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fn validate_selection(values: &[String], flag: &str, config: &Config) -> Result<()> {
|
|
673
|
+
let known: HashSet<&str> = config.known_cop_names().collect();
|
|
674
|
+
let departments: HashSet<&str> = known
|
|
675
|
+
.iter()
|
|
676
|
+
.map(|name| cop_name::department(name))
|
|
677
|
+
.collect();
|
|
678
|
+
for value in values {
|
|
679
|
+
if !known.contains(value.as_str()) && !departments.contains(value.as_str()) {
|
|
680
|
+
bail!("Unrecognized cop or department for {flag}: {value}.");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
Ok(())
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
fn debug_report(cwd: &Path, config: &Config, targets: usize, parallel: bool) {
|
|
687
|
+
let implemented: HashSet<&str> = rule_names().collect();
|
|
688
|
+
let mut unimplemented = config
|
|
689
|
+
.known_cop_names()
|
|
690
|
+
.filter(|name| !implemented.contains(name))
|
|
691
|
+
.collect::<Vec<_>>();
|
|
692
|
+
unimplemented.sort();
|
|
693
|
+
eprintln!(
|
|
694
|
+
"For {}: configuration from {}, {targets} target(s), parallel={parallel}",
|
|
695
|
+
cwd.display(),
|
|
696
|
+
config.config_path().map_or_else(
|
|
697
|
+
|| "built-in defaults".to_owned(),
|
|
698
|
+
|path| path.display().to_string()
|
|
699
|
+
)
|
|
700
|
+
);
|
|
701
|
+
eprintln!(
|
|
702
|
+
"Implemented cops: {}; recognized but not implemented: {}",
|
|
703
|
+
implemented.len(),
|
|
704
|
+
unimplemented.len()
|
|
705
|
+
);
|
|
706
|
+
eprintln!("Unimplemented cops: {}", unimplemented.join(", "));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/// The cop names a `--show-cops`/`--show-docs-url` filter selects, sorted. An empty filter names
|
|
710
|
+
/// every known cop, which is how RuboCop prints the whole list.
|
|
711
|
+
fn selected_cop_names<'a>(filter: &str, config: &'a Config) -> Vec<&'a str> {
|
|
712
|
+
let filters = csv((!filter.is_empty()).then_some(filter));
|
|
713
|
+
let mut names = config
|
|
714
|
+
.known_cop_names()
|
|
715
|
+
.filter(|name| {
|
|
716
|
+
filters.is_empty()
|
|
717
|
+
|| filters
|
|
718
|
+
.iter()
|
|
719
|
+
.any(|selection| selector_matches(selection, name))
|
|
720
|
+
})
|
|
721
|
+
.collect::<Vec<_>>();
|
|
722
|
+
names.sort_unstable();
|
|
723
|
+
names
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
fn show_cops(filter: &str, config: &Config) {
|
|
727
|
+
let implemented: HashSet<&str> = rule_names().collect();
|
|
728
|
+
for name in selected_cop_names(filter, config) {
|
|
729
|
+
println!("{name}:");
|
|
730
|
+
if let Some(description) = config.description(name) {
|
|
731
|
+
println!(
|
|
732
|
+
" Description: {}",
|
|
733
|
+
description.lines().next().unwrap_or("")
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
println!(" Enabled: {}", config.rule_enabled(name));
|
|
737
|
+
println!(" Implemented: {}", implemented.contains(name));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
fn show_docs_urls(filter: &str, config: &Config) {
|
|
742
|
+
for name in selected_cop_names(filter, config) {
|
|
743
|
+
println!("{name}: {}", docs_url(name));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// RuboCop's `Documentation.url_for`: the page covers the whole department with `/` flattened to
|
|
748
|
+
/// `_`, and the fragment is the qualified name reduced to its lowercased letters, so
|
|
749
|
+
/// `Layout/LineLength` documents at `cops_layout.html#layoutlinelength`.
|
|
750
|
+
fn docs_url(name: &str) -> String {
|
|
751
|
+
let page = cop_name::department(name)
|
|
752
|
+
.replace('/', "_")
|
|
753
|
+
.to_ascii_lowercase();
|
|
754
|
+
let fragment: String = name
|
|
755
|
+
.chars()
|
|
756
|
+
.filter(char::is_ascii_alphabetic)
|
|
757
|
+
.map(|character| character.to_ascii_lowercase())
|
|
758
|
+
.collect();
|
|
759
|
+
format!("https://docs.rubocop.org/rubocop/{RUBOCOP_COMPAT_VERSION}/cops_{page}.html#{fragment}")
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
fn list_enabled_cops(path: &Path, configs: &ConfigStore) -> Result<()> {
|
|
763
|
+
let config = configs.for_path(path)?;
|
|
764
|
+
let mut names = config
|
|
765
|
+
.known_cop_names()
|
|
766
|
+
.filter(|name| config.rule_enabled(name))
|
|
767
|
+
.collect::<Vec<_>>();
|
|
768
|
+
names.sort();
|
|
769
|
+
for name in names {
|
|
770
|
+
println!("{name}");
|
|
771
|
+
}
|
|
772
|
+
Ok(())
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn generate_todo(reports: &[FileReport], cwd: &Path, cli: &Cli) -> Result<()> {
|
|
776
|
+
let mut output = format!(
|
|
777
|
+
"# This configuration was generated by\n# `sonicop --auto-gen-config` using Sonicop version {VERSION}.\n\n"
|
|
778
|
+
);
|
|
779
|
+
for (cop, offenses) in offenses_by_cop(reports, cwd) {
|
|
780
|
+
// RuboCop drops Lint/Syntax before writing the records (`disabled_config_formatter.rb:69`)
|
|
781
|
+
// because it is not a real cop and cannot be disabled, so a record for it would only be
|
|
782
|
+
// noise the user can never resolve.
|
|
783
|
+
if is_mandatory_cop(cop) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if !cli.no_offense_counts {
|
|
787
|
+
output.push_str(&format!("# Offense count: {}\n", offenses.offense_count));
|
|
788
|
+
}
|
|
789
|
+
output.push_str(&format!("{cop}:\n"));
|
|
790
|
+
// The limit counts offending files, not offenses, so a single file cannot exclude itself
|
|
791
|
+
// into `Enabled: false`.
|
|
792
|
+
if !cli.no_exclude_limit && offenses.paths.len() > cli.exclude_limit {
|
|
793
|
+
output.push_str(" Enabled: false\n\n");
|
|
794
|
+
} else {
|
|
795
|
+
output.push_str(" Exclude:\n");
|
|
796
|
+
for path in &offenses.paths {
|
|
797
|
+
output.push_str(&format!(" - {}\n", yaml_single_quoted(path)));
|
|
798
|
+
}
|
|
799
|
+
output.push('\n');
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
fs::write(cwd.join(".rubocop_todo.yml"), output)?;
|
|
803
|
+
let root_config = cwd.join(".rubocop.yml");
|
|
804
|
+
let existing = fs::read_to_string(&root_config).unwrap_or_default();
|
|
805
|
+
if !existing.contains(".rubocop_todo.yml") {
|
|
806
|
+
let addition = if existing.is_empty() {
|
|
807
|
+
"inherit_from: .rubocop_todo.yml\n".to_owned()
|
|
808
|
+
} else {
|
|
809
|
+
format!("inherit_from: .rubocop_todo.yml\n{existing}")
|
|
810
|
+
};
|
|
811
|
+
fs::write(root_config, addition)?;
|
|
812
|
+
}
|
|
813
|
+
println!("Generated .rubocop_todo.yml");
|
|
814
|
+
Ok(())
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
fn filter_displayed_offenses(reports: &mut [FileReport], cli: &Cli, fail_level: FailLevel) {
|
|
818
|
+
for report in reports {
|
|
819
|
+
report.offenses.retain(|offense| {
|
|
820
|
+
(!cli.display_only_fail_level_offenses || offense.severity >= fail_level.severity())
|
|
821
|
+
&& (!cli.display_only_correctable || offense.is_correctable())
|
|
822
|
+
&& (!cli.display_only_safe_correctable
|
|
823
|
+
|| (offense.is_correctable()
|
|
824
|
+
&& offense.correction.as_ref().is_some_and(|edit| edit.safe)))
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/// The run-wide state every formatter needs, gathered once so each format only has to supply the
|
|
830
|
+
/// reports.
|
|
831
|
+
struct RenderRequest<'a> {
|
|
832
|
+
cli: &'a Cli,
|
|
833
|
+
config: &'a Config,
|
|
834
|
+
cwd: &'a Path,
|
|
835
|
+
fail_level: FailLevel,
|
|
836
|
+
/// Destination per format, already paired the way RuboCop pairs `--out` with `--format`.
|
|
837
|
+
outputs: &'a [Option<PathBuf>],
|
|
838
|
+
corrected_count: usize,
|
|
839
|
+
elapsed: f64,
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
fn render_outputs(request: &RenderRequest<'_>, reports: &[FileReport]) -> Result<()> {
|
|
843
|
+
let RenderRequest {
|
|
844
|
+
cli,
|
|
845
|
+
config,
|
|
846
|
+
cwd,
|
|
847
|
+
fail_level,
|
|
848
|
+
outputs,
|
|
849
|
+
corrected_count,
|
|
850
|
+
elapsed,
|
|
851
|
+
} = *request;
|
|
852
|
+
let formats = if cli.formats.is_empty() {
|
|
853
|
+
vec![
|
|
854
|
+
config
|
|
855
|
+
.all_cops_value::<String>("DefaultFormatter")
|
|
856
|
+
.unwrap_or_else(|| "progress".to_owned()),
|
|
857
|
+
]
|
|
858
|
+
} else {
|
|
859
|
+
cli.formats.clone()
|
|
860
|
+
};
|
|
861
|
+
let display_cop_names = if cli.no_display_cop_names {
|
|
862
|
+
false
|
|
863
|
+
} else {
|
|
864
|
+
cli.display_cop_names || config.display_cop_names()
|
|
865
|
+
};
|
|
866
|
+
let color = !cli.no_color
|
|
867
|
+
&& (cli.color || (cli.out.is_empty() && !cli.stderr && io::stdout().is_terminal()));
|
|
868
|
+
for (index, name) in formats.iter().enumerate() {
|
|
869
|
+
let format = Format::parse(name)?;
|
|
870
|
+
let mut rendered = render(
|
|
871
|
+
format,
|
|
872
|
+
reports,
|
|
873
|
+
&FormatOptions {
|
|
874
|
+
cwd,
|
|
875
|
+
config,
|
|
876
|
+
display_cop_names,
|
|
877
|
+
display_style_guide: cli.display_style_guide,
|
|
878
|
+
extra_details: cli.extra_details,
|
|
879
|
+
color,
|
|
880
|
+
corrected_count,
|
|
881
|
+
fail_level: fail_level.severity(),
|
|
882
|
+
safe_autocorrect: cli.correct_mode() == CorrectMode::Safe,
|
|
883
|
+
},
|
|
884
|
+
)?;
|
|
885
|
+
if cli.display_time {
|
|
886
|
+
rendered.push_str(&format!("Finished in {elapsed:.3} seconds\n"));
|
|
887
|
+
}
|
|
888
|
+
write_output(
|
|
889
|
+
&rendered,
|
|
890
|
+
outputs.get(index).and_then(Option::as_deref),
|
|
891
|
+
cli.stderr,
|
|
892
|
+
)?;
|
|
893
|
+
}
|
|
894
|
+
Ok(())
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
fn init_config(cwd: &Path) -> Result<i32> {
|
|
898
|
+
let path = cwd.join(".rubocop.yml");
|
|
899
|
+
if path.exists() {
|
|
900
|
+
bail!("{} already exists", path.display());
|
|
901
|
+
}
|
|
902
|
+
fs::write(
|
|
903
|
+
&path,
|
|
904
|
+
"# Sonicop / RuboCop configuration\nAllCops:\n NewCops: enable\n",
|
|
905
|
+
)?;
|
|
906
|
+
println!("Created {}", path.display());
|
|
907
|
+
Ok(0)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
fn csv(value: Option<&str>) -> Vec<String> {
|
|
911
|
+
value
|
|
912
|
+
.into_iter()
|
|
913
|
+
.flat_map(|value| value.split(','))
|
|
914
|
+
.map(str::trim)
|
|
915
|
+
.filter(|value| !value.is_empty())
|
|
916
|
+
.map(ToOwned::to_owned)
|
|
917
|
+
.collect()
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
fn write_output(output: &str, path: Option<&Path>, stderr: bool) -> Result<()> {
|
|
921
|
+
if let Some(path) = path {
|
|
922
|
+
fs::write(path, output).with_context(|| format!("failed to write {}", path.display()))?;
|
|
923
|
+
} else if stderr {
|
|
924
|
+
io::stderr().write_all(output.as_bytes())?;
|
|
925
|
+
} else {
|
|
926
|
+
io::stdout().write_all(output.as_bytes())?;
|
|
927
|
+
}
|
|
928
|
+
Ok(())
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#[cfg(test)]
|
|
932
|
+
mod tests {
|
|
933
|
+
use super::Cli;
|
|
934
|
+
use clap::CommandFactory;
|
|
935
|
+
|
|
936
|
+
#[test]
|
|
937
|
+
fn cli_definition_is_valid() {
|
|
938
|
+
Cli::command().debug_assert();
|
|
939
|
+
}
|
|
940
|
+
}
|