Skip to content

Commit

Permalink
Luacheck mode for existing consumers (Kampfkarren#26)
Browse files Browse the repository at this point in the history
* Base commit, ignore unknown parameters when in luacheck mode.

* Test when luacheck atomic is true

* Fix clippy lint

* Format correctly, don't show results

* Start the process when named luacheck

* Add --ranges option

* Add stdin support, fix --formatter=plain

* Wrap around full lines correctly

* Multiple line spans

* Rustfmt
  • Loading branch information
Kampfkarren authored Nov 6, 2019
1 parent fce8dae commit cf28dd8
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 59 deletions.
14 changes: 7 additions & 7 deletions selene-lib/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ pub enum Severity {
}

pub struct Diagnostic {
code: &'static str,
message: String,
notes: Vec<String>,
primary_label: Label,
secondary_labels: Vec<Label>,
pub code: &'static str,
pub message: String,
pub notes: Vec<String>,
pub primary_label: Label,
pub secondary_labels: Vec<Label>,
}

impl Diagnostic {
Expand Down Expand Up @@ -124,8 +124,8 @@ impl Diagnostic {
}

pub struct Label {
message: Option<String>,
range: (u32, u32),
pub message: Option<String>,
pub range: (u32, u32),
}

impl Label {
Expand Down
1 change: 1 addition & 0 deletions selene/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ edition = "2018"
codespan = "0.4"
codespan-reporting = "0.4"
full_moon = "0.4.0-rc.11"
lazy_static = "1.4"
glob = "0.3"
selene-lib = { path = "../selene-lib" }
num_cpus = "1.10"
Expand Down
298 changes: 247 additions & 51 deletions selene/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use std::{
ffi::OsString,
fmt, fs,
io::{self, Write},
io::{self, Read, Write},
path::Path,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
},
};

use codespan_reporting::{diagnostic::Severity as CodespanSeverity, term::DisplayStyle};
use full_moon::ast::owned::Owned;
use selene_lib::{rules::Severity, standard_library::StandardLibrary, *};
use structopt::StructOpt;
use structopt::{clap, StructOpt};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use threadpool::ThreadPool;

Expand All @@ -27,7 +28,9 @@ macro_rules! error {
};
}

static QUIET: AtomicBool = AtomicBool::new(false);
lazy_static::lazy_static! {
static ref OPTIONS: RwLock<Option<opts::Options>> = RwLock::new(None);
}

static LINT_ERRORS: AtomicUsize = AtomicUsize::new(0);
static LINT_WARNINGS: AtomicUsize = AtomicUsize::new(0);
Expand Down Expand Up @@ -67,18 +70,20 @@ fn log_total(parse_errors: usize, lint_errors: usize, lint_warnings: usize) -> i
Ok(())
}

fn read_file(checker: &Checker<toml::value::Value>, filename: &Path) {
let contents = match fs::read_to_string(filename) {
Ok(contents) => contents,
Err(error) => {
error!(
"Couldn't read contents of file {}: {}",
filename.display(),
error
);
return;
}
};
fn read<R: Read>(checker: &Checker<toml::value::Value>, filename: &Path, mut reader: R) {
let mut buffer = Vec::new();
if let Err(error) = reader.read_to_end(&mut buffer) {
error!(
"Couldn't read contents of file {}: {}",
filename.display(),
error,
);
}

let contents = String::from_utf8_lossy(&buffer);

let lock = OPTIONS.read().unwrap();
let opts = lock.as_ref().unwrap();

let ast = match full_moon::parse(&contents) {
Ok(ast) => ast.owned(),
Expand Down Expand Up @@ -110,36 +115,122 @@ fn read_file(checker: &Checker<toml::value::Value>, filename: &Path) {
LINT_ERRORS.fetch_add(errors, Ordering::Release);
LINT_WARNINGS.fetch_add(warnings, Ordering::Release);

for diagnostic in diagnostics.into_iter().map(|diagnostic| {
diagnostic.diagnostic.into_codespan_diagnostic(
source_id,
match diagnostic.severity {
Severity::Error => CodespanSeverity::Error,
Severity::Warning => CodespanSeverity::Warning,
},
)
}) {
codespan_reporting::term::emit(
&mut stdout,
&codespan_reporting::term::Config {
display_style: if QUIET.load(Ordering::Relaxed) {
DisplayStyle::Short
} else {
DisplayStyle::Rich
for diagnostic in diagnostics {
if opts.luacheck {
// Existing Luacheck consumers presumably use --formatter plain
let primary_label = &diagnostic.diagnostic.primary_label;
let end = files.location(source_id, primary_label.range.1).unwrap();

// Closures in Rust cannot call themselves recursively, especially not mutable ones.
// Luacheck only allows one line ranges, so we just repeat the lint for every line it spans.
// This would be frustrating for a human to read, but consumers (editors) will instead show it
// as a native implementation would.
let mut stack = Vec::new();

let mut write = |stack: &mut Vec<_>, start: codespan::Location| -> io::Result<()> {
write!(stdout, "{}:", filename.display())?;
write!(stdout, "{}:{}", start.line.number(), start.column.number())?;

if opts.ranges {
write!(
stdout,
"-{}",
if start.line != end.line {
// Report to the end of the line
files
.source(source_id)
.lines()
.nth(start.line.to_usize())
.unwrap()
.chars()
.count()
} else {
end.column.to_usize()
}
)?;
}

// The next line will be displayed just like this one
if start.line != end.line {
stack.push(codespan::Location::new(
(start.line.to_usize() + 1) as u32,
0,
));
}

write!(
stdout,
": ({}000) ",
match diagnostic.severity {
Severity::Error => "E",
Severity::Warning => "W",
}
)?;

write!(stdout, "[{}] ", diagnostic.diagnostic.code)?;
write!(stdout, "{}", diagnostic.diagnostic.message)?;

if !diagnostic.diagnostic.notes.is_empty() {
write!(stdout, "\n{}", diagnostic.diagnostic.notes.join("\n"))?;
}

writeln!(stdout)?;
Ok(())
};

write(
&mut stack,
files.location(source_id, primary_label.range.0).unwrap(),
)
.unwrap();

while let Some(new_start) = stack.pop() {
write(&mut stack, new_start).unwrap();
}
} else {
let diagnostic = diagnostic.diagnostic.into_codespan_diagnostic(
source_id,
match diagnostic.severity {
Severity::Error => CodespanSeverity::Error,
Severity::Warning => CodespanSeverity::Warning,
},
..Default::default()
},
&files,
&diagnostic,
)
.expect("couldn't emit to codespan");
);

codespan_reporting::term::emit(
&mut stdout,
&codespan_reporting::term::Config {
display_style: if opts.quiet {
DisplayStyle::Short
} else {
DisplayStyle::Rich
},
..Default::default()
},
&files,
&diagnostic,
)
.expect("couldn't emit to codespan");
}
}
}

fn main() {
let matches = opts::Options::from_args();
fn read_file(checker: &Checker<toml::value::Value>, filename: &Path) {
read(
checker,
filename,
match fs::File::open(filename) {
Ok(file) => file,
Err(error) => {
error!("Couldn't open file {}: {}", filename.display(), error,);

return;
}
},
);
}

QUIET.store(matches.quiet, Ordering::Relaxed);
fn start(matches: opts::Options) {
*OPTIONS.write().unwrap() = Some(matches.clone());

let config: CheckerConfig<toml::value::Value> = match matches.config {
Some(config_file) => {
Expand Down Expand Up @@ -208,6 +299,12 @@ fn main() {
let pool = ThreadPool::new(matches.num_threads);

for filename in &matches.files {
if filename == "-" {
let checker = Arc::clone(&checker);
pool.execute(move || read(&checker, Path::new("-"), io::stdin().lock()));
continue;
}

match fs::metadata(filename) {
Ok(metadata) => {
if metadata.is_file() {
Expand All @@ -216,14 +313,17 @@ fn main() {

pool.execute(move || read_file(&checker, Path::new(&filename)));
} else if metadata.is_dir() {
let glob =
match glob::glob(&format!("{}/{}", filename.to_string_lossy(), matches.pattern)) {
Ok(glob) => glob,
Err(error) => {
error!("Invalid glob pattern: {}", error);
return;
}
};
let glob = match glob::glob(&format!(
"{}/{}",
filename.to_string_lossy(),
matches.pattern,
)) {
Ok(glob) => glob,
Err(error) => {
error!("Invalid glob pattern: {}", error);
return;
}
};

for entry in glob {
match entry {
Expand Down Expand Up @@ -265,9 +365,105 @@ fn main() {
LINT_WARNINGS.load(Ordering::Relaxed),
);

log_total(parse_errors, lint_errors, lint_warnings).ok();
if !matches.luacheck {
log_total(parse_errors, lint_errors, lint_warnings).ok();
}

if parse_errors + lint_errors + lint_warnings > 0 {
std::process::exit(1);
}
}

fn main() {
let mut luacheck = false;

if let Ok(path) = std::env::current_exe() {
if let Some(stem) = path.file_stem() {
if stem.to_str() == Some("luacheck") {
luacheck = true;
}
}
}

start(get_opts(luacheck));
}

// Will attempt to get the options.
// Different from Options::from_args() as if in Luacheck mode
// (either found from --luacheck or from the LUACHECK AtomicBool)
// it will ignore all extra parameters.
// If not in luacheck mode and errors are found, will exit.
fn get_opts(luacheck: bool) -> opts::Options {
get_opts_safe(std::env::args_os().collect::<Vec<_>>(), luacheck)
.unwrap_or_else(|err| err.exit())
}

fn get_opts_safe(mut args: Vec<OsString>, luacheck: bool) -> Result<opts::Options, clap::Error> {
let mut first_error: Option<clap::Error> = None;

loop {
match opts::Options::from_iter_safe(&args) {
Ok(mut options) => match first_error {
Some(error) => {
if options.luacheck || luacheck {
options.luacheck = true;
break Ok(options);
} else {
break Err(error);
}
}

None => break Ok(options),
},

Err(err) => match err.kind {
clap::ErrorKind::UnknownArgument => {
let bad_arg =
&err.info.as_ref().expect("no info for UnknownArgument")[0].to_owned();

args = args
.drain(..)
.filter(|arg| arg.to_string_lossy().split('=').next().unwrap() != bad_arg)
.collect();

if first_error.is_none() {
first_error = Some(err);
}
}

_ => break Err(err),
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn args(mut args: Vec<&str>) -> Vec<OsString> {
args.insert(0, "selene");
args.into_iter().map(OsString::from).collect()
}

#[test]
fn test_luacheck_opts() {
assert!(get_opts_safe(args(vec!["file"]), false).is_ok());
assert!(get_opts_safe(args(vec!["--fail", "files"]), false).is_err());

match get_opts_safe(args(vec!["--luacheck", "--fail", "files"]), false) {
Ok(opts) => {
assert!(opts.luacheck);
assert_eq!(opts.files, vec![OsString::from("files")]);
}

Err(err) => {
panic!("selene --luacheck --fail files returned Err: {:?}", err);
}
}

assert!(get_opts_safe(args(vec!["-", "--formatter=plain"]), true).is_ok());

assert!(get_opts_safe(args(vec!["--fail", "files"]), true).is_ok());
}
}
Loading

0 comments on commit cf28dd8

Please sign in to comment.