#![feature(box_syntax)]
#![cfg_attr(feature = "gazebo_lint", feature(plugin))]
#![cfg_attr(feature = "gazebo_lint", allow(deprecated))] #![cfg_attr(feature = "gazebo_lint", plugin(gazebo_lint))]
#![allow(clippy::type_complexity)]
use std::{ffi::OsStr, fmt, fmt::Display, path::PathBuf, sync::Arc};
use anyhow::anyhow;
use eval::Context;
use gazebo::prelude::*;
use itertools::Either;
use starlark::read_line::ReadLine;
use structopt::{clap::AppSettings, StructOpt};
use walkdir::WalkDir;
use crate::{
eval::ContextMode,
types::{LintMessage, Message, Severity},
};
mod dap;
mod eval;
mod lsp;
mod types;
#[derive(Debug, StructOpt)]
#[structopt(
name = "starlark",
about = "Evaluate Starlark code",
global_settings(&[AppSettings::ColoredHelp]),
)]
struct Args {
#[structopt(
long = "interactive",
long = "repl",
short = "i",
help = "Start an interactive REPL."
)]
interactive: bool,
#[structopt(long = "lsp", help = "Start an LSP server.")]
lsp: bool,
#[structopt(long = "dap", help = "Start a DAP server.")]
dap: bool,
#[structopt(
long = "check",
help = "Run checks and lints.",
conflicts_with_all = &["lsp", "dap"],
)]
check: bool,
#[structopt(
long = "json",
help = "Show output as JSON lines.",
conflicts_with_all = &["lsp", "dap"],
)]
json: bool,
#[structopt(
long = "extension",
help = "File extension when searching directories."
)]
extension: Option<String>,
#[structopt(long = "prelude", help = "Files to load in advance.")]
prelude: Vec<PathBuf>,
#[structopt(
long = "expression",
short = "e",
name = "EXPRESSION",
help = "Expressions to evaluate.",
conflicts_with_all = &["lsp", "dap"],
)]
evaluate: Vec<String>,
#[structopt(
name = "FILE",
help = "Files to evaluate.",
conflicts_with_all = &["lsp", "dap"],
)]
files: Vec<PathBuf>,
}
fn expand_dirs(extension: &str, xs: Vec<PathBuf>) -> impl Iterator<Item = PathBuf> {
let extension = Arc::new(extension.to_owned());
xs.into_iter().flat_map(move |x| {
let extension = extension.dupe();
if x.is_dir() {
Either::Left(
WalkDir::new(x)
.into_iter()
.filter_map(|e| e.ok())
.filter(move |e| e.path().extension() == Some(OsStr::new(extension.as_str())))
.map(|e| e.into_path()),
)
} else {
Either::Right(box vec![x].into_iter())
}
})
}
#[derive(Default)]
struct Stats {
file: usize,
error: usize,
warning: usize,
advice: usize,
disabled: usize,
}
impl Display for Stats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&format!(
"{} files, {} errors, {} warnings, {} advices, {} disabled",
self.file, self.error, self.warning, self.advice, self.disabled
))
}
}
impl Stats {
fn increment_file(&mut self) {
self.file += 1;
}
fn increment(&mut self, x: Severity) {
match x {
Severity::Error => self.error += 1,
Severity::Warning => self.warning += 1,
Severity::Advice => self.advice += 1,
Severity::Disabled => self.disabled += 1,
}
}
}
fn drain(xs: impl Iterator<Item = Message>, json: bool, stats: &mut Stats) {
for x in xs {
stats.increment(x.severity);
if json {
println!("{}", serde_json::to_string(&LintMessage::new(x)).unwrap());
} else if let Some(error) = x.full_error_with_span {
let mut error = error.to_owned();
if !error.is_empty() && !error.ends_with('\n') {
error.push('\n');
}
print!("{}", error);
} else {
println!("{}", x);
}
}
}
fn interactive(ctx: &Context) -> anyhow::Result<()> {
let mut rl = ReadLine::new("STARLARK_RUST_HISTFILE");
loop {
match rl.read_line("$> ")? {
Some(line) => {
let mut stats = Stats::default();
drain(ctx.expression(line), false, &mut stats);
}
None => return Ok(()),
}
}
}
fn main() -> anyhow::Result<()> {
gazebo::terminate_on_panic();
let args = argfile::expand_args(argfile::parse_fromfile, argfile::PREFIX)?;
let args = Args::from_iter(args);
let ext = args
.extension
.as_ref()
.map_or("bzl", |x| x.as_str())
.trim_start_match('.');
let mut ctx = Context::new(
if args.check {
ContextMode::Check
} else {
ContextMode::Run
},
!args.evaluate.is_empty() || args.interactive,
&expand_dirs(ext, args.prelude).collect::<Vec<_>>(),
args.interactive,
)?;
let mut stats = Stats::default();
for e in args.evaluate.clone() {
stats.increment_file();
drain(ctx.expression(e), args.json, &mut stats);
}
for file in expand_dirs(ext, args.files.clone()) {
stats.increment_file();
drain(ctx.file(&file), args.json, &mut stats);
}
if args.interactive {
interactive(&ctx)?;
}
if args.lsp {
ctx.mode = ContextMode::Check;
lsp::server(ctx)?;
} else if args.dap {
dap::server()
}
if !args.json {
println!("{}", stats);
if stats.error > 0 {
return Err(anyhow!("Failed with {} errors", stats.error));
}
}
Ok(())
}