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
use std::{collections::HashSet, convert::TryFrom, io::Write};
use anyhow::{Context, Result};
use clap::Parser;
use lintrunner::{
do_init, do_lint,
init::check_init_changed,
lint_config::{get_linters_from_config, LintRunnerConfig},
path::AbsPath,
persistent_data::PersistentDataStore,
render::print_error,
PathsOpt, RenderOpt, RevisionOpt,
};
#[derive(Debug, Parser)]
#[clap(name = "lintrunner", about = "A lint runner", infer_subcommands(true))]
struct Args {
/// Verbose mode (-v, or -vv to show full list of paths being linted)
#[clap(short, long, parse(from_occurrences), global = true)]
verbose: u8,
/// Path to a toml file defining which linters to run
#[clap(long, default_value = ".lintrunner.toml", global = true)]
config: String,
/// If set, any suggested patches will be applied
#[clap(short, long, global = true)]
apply_patches: bool,
/// Shell command that returns new-line separated paths to lint
///
/// Example: To run on all files in the repo, use `--paths-cmd='git grep -Il .'`.
#[clap(long, conflicts_with = "paths-from", global = true)]
paths_cmd: Option<String>,
/// File with new-line separated paths to lint
#[clap(long, global = true)]
paths_from: Option<String>,
/// Lint all files that differ between the working directory and the
/// specified revision. This argument can be any <tree-ish> that is accepted
/// by `git diff-tree`
#[clap(long, short, conflicts_with_all=&["paths", "paths-cmd", "paths-from"], global = true)]
revision: Option<String>,
/// Lint all files that differ between the merge base of HEAD with the
/// specified revision and HEAD. This argument can be any <tree-sh> that is
/// accepted by `git diff-tree`
///
/// Example: lintrunner -m master
#[clap(long, short, conflicts_with_all=&["paths", "paths-cmd", "paths-from", "revision"], global = true)]
merge_base_with: Option<String>,
/// Comma-separated list of linters to skip (e.g. --skip CLANGFORMAT,NOQA)
#[clap(long, global = true)]
skip: Option<String>,
/// Comma-separated list of linters to run (opposite of --skip)
#[clap(long, global = true)]
take: Option<String>,
/// With 'default' show lint issues in human-readable format, for interactive use.
/// With 'json', show lint issues as machine-readable JSON (one per line)
/// With 'oneline', show lint issues in compact format (one per line)
#[clap(long, arg_enum, default_value_t = RenderOpt::Default, global=true)]
output: RenderOpt,
/// Paths to lint. lintrunner will still respect the inclusions and
#[clap(subcommand)]
cmd: Option<SubCommand>,
/// exclusions defined in .lintrunner.toml; manually specifying a path will
/// not override them.
#[clap(conflicts_with_all = &["paths-cmd", "paths-from"], global = true)]
paths: Vec<String>,
/// If set, always output with ANSI colors, even if we detect the output is
/// not a user-attended terminal.
#[clap(long, global = true)]
force_color: bool,
/// If set, use ths provided path to store any metadata generated by
/// lintrunner. By default, this is a platform-specific location for
/// application data (e.g. $XDG_DATA_HOME for UNIX systems.)
#[clap(long, global = true)]
data_path: Option<String>,
}
#[derive(Debug, Parser)]
enum SubCommand {
/// Perform first-time setup for linters
Init {
/// If set, do not actually execute initialization commands, just print them
#[clap(long, short)]
dry_run: bool,
},
/// Run and accept changes for formatting linters only. Equivalent to
/// `lintrunner --apply-patches --take <formatters>`.
Format,
/// Run linters. This is the default if no subcommand is provided.
Lint,
}
fn do_main() -> Result<i32> {
let args = Args::parse();
if args.force_color {
console::set_colors_enabled(true);
console::set_colors_enabled_stderr(true);
}
let log_level = match (args.verbose, args.output != RenderOpt::Default) {
// Default
(0, false) => log::LevelFilter::Info,
// If just json is asked for, suppress most output except hard errors.
(0, true) => log::LevelFilter::Error,
// Verbose overrides json.
(1, false) => log::LevelFilter::Debug,
(1, true) => log::LevelFilter::Debug,
// Any higher verbosity goes to trace.
(_, _) => log::LevelFilter::Trace,
};
env_logger::Builder::new().filter_level(log_level).init();
let config_path = AbsPath::try_from(&args.config)
.with_context(|| format!("Could not read lintrunner config at: '{}'", args.config))?;
let cmd = args.cmd.unwrap_or(SubCommand::Lint);
let lint_runner_config = LintRunnerConfig::new(&config_path)?;
let skipped_linters = args.skip.map(|linters| {
linters
.split(',')
.map(|linter_name| linter_name.to_string())
.collect::<HashSet<_>>()
});
let taken_linters = args.take.map(|linters| {
linters
.split(',')
.map(|linter_name| linter_name.to_string())
.collect::<HashSet<_>>()
});
// If we are formatting, the universe of linters to select from should be
// restricted to only formatters.
// (NOTE: we pay an allocation for `placeholder` even in cases where we are
// just passing through a reference in the else-branch. This doesn't matter,
// but if we want to fix it we should impl Cow for LintConfig and use that
// instead.).
let mut placeholder = Vec::new();
let all_linters = if let SubCommand::Format = &cmd {
let iter = lint_runner_config
.linters
.iter()
.filter(|l| l.is_formatter)
.cloned();
placeholder.extend(iter);
&placeholder
} else {
// If we're not formatting, all linters defined in the config are
// eligible to run.
&lint_runner_config.linters
};
let linters =
get_linters_from_config(all_linters, skipped_linters, taken_linters, &config_path)?;
let enable_spinners = args.verbose == 0 && args.output == RenderOpt::Default;
let persistent_data_store = PersistentDataStore::new(&config_path)?;
let revision_opt = if let Some(revision) = args.revision {
RevisionOpt::Revision(revision)
} else if let Some(merge_base_with) = args.merge_base_with {
RevisionOpt::MergeBaseWith(merge_base_with)
} else {
RevisionOpt::Head
};
let paths_opt = if let Some(paths_file) = args.paths_from {
let path_file = AbsPath::try_from(&paths_file)
.with_context(|| format!("Failed to find `--paths-from` file '{}'", paths_file))?;
PathsOpt::PathsFile(path_file)
} else if let Some(paths_cmd) = args.paths_cmd {
PathsOpt::PathsCmd(paths_cmd)
} else if !args.paths.is_empty() {
PathsOpt::Paths(args.paths)
} else {
PathsOpt::Auto
};
match cmd {
SubCommand::Init { dry_run } => {
// Just run initialization commands, don't actually lint.
do_init(linters, dry_run, &persistent_data_store, &config_path)
}
SubCommand::Format => {
check_init_changed(&persistent_data_store, &lint_runner_config)?;
do_lint(
linters,
paths_opt,
true, // always apply patches when we use the format command
args.output,
enable_spinners,
revision_opt,
)
}
SubCommand::Lint => {
// Default command is to just lint.
check_init_changed(&persistent_data_store, &lint_runner_config)?;
do_lint(
linters,
paths_opt,
args.apply_patches,
args.output,
enable_spinners,
revision_opt,
)
}
}
}
fn main() {
let code = match do_main() {
Ok(code) => code,
Err(err) => {
print_error(&err)
.context("failed to print exit error")
.unwrap();
1
}
};
// Flush the output before exiting, in case there is anything left in the buffers.
drop(std::io::stdout().flush());
drop(std::io::stderr().flush());
// exit() abruptly ends the process while running no destructors. We should
// make sure that nothing is alive before running this.
std::process::exit(code);
}