use std::{
collections::HashMap,
env,
error::Error,
fs::{self, File},
io,
path::{Path, PathBuf},
sync::Arc,
};
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use miette::{LabeledSpan, MietteDiagnostic, NamedSource};
use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator};
use crate::{
config::{Config, Req},
definitions::{ContractType, ItemType},
files::find_sol_files,
lint::{FileDiagnostics, ItemDiagnostics, ValidationOptions, lint},
parser::Parse as _,
};
#[cfg(feature = "slang")]
use crate::parser::slang::SlangParser;
#[cfg(feature = "solar")]
use crate::parser::solar::SolarParser;
#[cfg(not(feature = "slang"))]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "slang")]
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-slang");
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
Init,
Completions {
#[arg(short, long)]
shell: Shell,
#[arg(short, long, value_hint = clap::ValueHint::DirPath)]
out: Option<PathBuf>,
},
}
#[derive(Parser, Debug, Clone)]
#[command(version = VERSION, about, long_about = None)]
#[non_exhaustive]
pub struct Args {
#[arg(name = "PATH", value_hint = clap::ValueHint::AnyPath)]
pub paths: Vec<PathBuf>,
#[arg(short, long, value_hint = clap::ValueHint::AnyPath)]
pub exclude: Vec<PathBuf>,
#[arg(long, value_hint = clap::ValueHint::FilePath)]
pub config: Option<PathBuf>,
#[arg(short, long, value_hint = clap::ValueHint::FilePath)]
pub out: Option<PathBuf>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub inheritdoc: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub inheritdoc_override: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub notice_or_dev: Option<bool>,
#[cfg(feature = "slang")]
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub skip_version_detection: Option<bool>,
#[arg(long)]
pub title_ignored: Vec<ContractType>,
#[arg(long)]
pub title_required: Vec<ContractType>,
#[arg(long)]
pub title_forbidden: Vec<ContractType>,
#[arg(long)]
pub author_ignored: Vec<ContractType>,
#[arg(long)]
pub author_required: Vec<ContractType>,
#[arg(long)]
pub author_forbidden: Vec<ContractType>,
#[arg(long)]
pub notice_ignored: Vec<ItemType>,
#[arg(long)]
pub notice_required: Vec<ItemType>,
#[arg(long)]
pub notice_forbidden: Vec<ItemType>,
#[arg(long)]
pub dev_ignored: Vec<ItemType>,
#[arg(long)]
pub dev_required: Vec<ItemType>,
#[arg(long)]
pub dev_forbidden: Vec<ItemType>,
#[arg(long)]
pub param_ignored: Vec<ItemType>,
#[arg(long)]
pub param_required: Vec<ItemType>,
#[arg(long)]
pub param_forbidden: Vec<ItemType>,
#[arg(long)]
pub return_ignored: Vec<ItemType>,
#[arg(long)]
pub return_required: Vec<ItemType>,
#[arg(long)]
pub return_forbidden: Vec<ItemType>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub json: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub compact: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub sort: Option<bool>,
#[command(subcommand)]
pub command: Option<Commands>,
}
macro_rules! cli_rule_override {
($config:expr, $items:expr, param, $req:expr) => {
for item in $items {
match item {
ItemType::Constructor => $config.constructors.param = $req,
ItemType::Enum => $config.enums.param = $req,
ItemType::Error => $config.errors.param = $req,
ItemType::Event => $config.events.param = $req,
ItemType::PrivateFunction => $config.functions.private.param = $req,
ItemType::InternalFunction => $config.functions.internal.param = $req,
ItemType::PublicFunction => $config.functions.public.param = $req,
ItemType::ExternalFunction => $config.functions.external.param = $req,
ItemType::Modifier => $config.modifiers.param = $req,
ItemType::Struct => $config.structs.param = $req,
_ => {}
}
}
};
($config:expr, $items:expr, return, $req:expr) => {
for item in $items {
match item {
ItemType::PrivateFunction => $config.functions.private.returns = $req,
ItemType::InternalFunction => $config.functions.internal.returns = $req,
ItemType::PublicFunction => $config.functions.public.returns = $req,
ItemType::ExternalFunction => $config.functions.external.returns = $req,
ItemType::PublicVariable => $config.variables.public.returns = $req,
_ => {}
}
}
};
($config:expr, $items:expr, title, $req:expr) => {
for item in $items {
match item {
ContractType::Contract => $config.contracts.title = $req,
ContractType::Interface => $config.interfaces.title = $req,
ContractType::Library => $config.libraries.title = $req,
}
}
};
($config:expr, $items:expr, author, $req:expr) => {
for item in $items {
match item {
ContractType::Contract => $config.contracts.author = $req,
ContractType::Interface => $config.interfaces.author = $req,
ContractType::Library => $config.libraries.author = $req,
}
}
};
($config:expr, $items:expr, $tag:ident, $req:expr) => {
for item in $items {
match item {
ItemType::Contract => $config.contracts.$tag = $req,
ItemType::Interface => $config.interfaces.$tag = $req,
ItemType::Library => $config.libraries.$tag = $req,
ItemType::Constructor => $config.constructors.$tag = $req,
ItemType::Enum => $config.enums.$tag = $req,
ItemType::Error => $config.errors.$tag = $req,
ItemType::Event => $config.events.$tag = $req,
ItemType::PrivateFunction => $config.functions.private.$tag = $req,
ItemType::InternalFunction => $config.functions.internal.$tag = $req,
ItemType::PublicFunction => $config.functions.public.$tag = $req,
ItemType::ExternalFunction => $config.functions.external.$tag = $req,
ItemType::Modifier => $config.modifiers.$tag = $req,
ItemType::Struct => $config.structs.$tag = $req,
ItemType::PrivateVariable => $config.variables.private.$tag = $req,
ItemType::InternalVariable => $config.variables.internal.$tag = $req,
ItemType::PublicVariable => $config.variables.public.$tag = $req,
ItemType::ParsingError => {}
}
}
};
}
pub fn read_config(args: Args) -> Result<Config, Box<figment::Error>> {
let config_path = args
.config
.or_else(|| env::var("LS_CONFIG_PATH").ok().map(Into::into));
let mut config: Config = Config::figment(config_path).extract()?;
config.lintspec.paths.extend(args.paths);
config.lintspec.exclude.extend(args.exclude);
if let Some(out) = args.out {
config.output.out = Some(out);
}
if let Some(json) = args.json {
config.output.json = json;
}
if let Some(compact) = args.compact {
config.output.compact = compact;
}
if let Some(sort) = args.sort {
config.output.sort = sort;
}
#[cfg(feature = "slang")]
if let Some(skip_version_detection) = args.skip_version_detection {
config.lintspec.skip_version_detection = skip_version_detection;
}
if let Some(inheritdoc) = args.inheritdoc {
config.lintspec.inheritdoc = inheritdoc;
}
if let Some(inheritdoc_override) = args.inheritdoc_override {
config.lintspec.inheritdoc_override = inheritdoc_override;
}
if let Some(notice_or_dev) = args.notice_or_dev {
config.lintspec.notice_or_dev = notice_or_dev;
}
cli_rule_override!(config, args.title_ignored, title, Req::Ignored);
cli_rule_override!(config, args.title_required, title, Req::Required);
cli_rule_override!(config, args.title_forbidden, title, Req::Forbidden);
cli_rule_override!(config, args.author_ignored, author, Req::Ignored);
cli_rule_override!(config, args.author_required, author, Req::Required);
cli_rule_override!(config, args.author_forbidden, author, Req::Forbidden);
cli_rule_override!(config, args.notice_ignored, notice, Req::Ignored);
cli_rule_override!(config, args.notice_required, notice, Req::Required);
cli_rule_override!(config, args.notice_forbidden, notice, Req::Forbidden);
cli_rule_override!(config, args.dev_ignored, dev, Req::Ignored);
cli_rule_override!(config, args.dev_required, dev, Req::Required);
cli_rule_override!(config, args.dev_forbidden, dev, Req::Forbidden);
cli_rule_override!(config, args.param_ignored, param, Req::Ignored);
cli_rule_override!(config, args.param_required, param, Req::Required);
cli_rule_override!(config, args.param_forbidden, param, Req::Forbidden);
cli_rule_override!(config, args.return_ignored, return, Req::Ignored);
cli_rule_override!(config, args.return_required, return, Req::Required);
cli_rule_override!(config, args.return_forbidden, return, Req::Forbidden);
Ok(config)
}
pub enum RunResult {
NoDiagnostics,
SomeDiagnostics,
}
pub fn run(config: &Config) -> Result<RunResult, Box<dyn Error>> {
let paths = find_sol_files(
&config.lintspec.paths,
&config.lintspec.exclude,
config.output.sort,
)?;
if paths.is_empty() {
return Err(String::from("no Solidity file found, nothing to analyze").into());
}
let options: ValidationOptions = config.into();
#[cfg_attr(all(feature = "slang", feature = "slang"), expect(unused_variables))]
#[cfg(feature = "solar")]
let parser = SolarParser::default();
#[cfg(feature = "slang")]
let parser = SlangParser::builder()
.skip_version_detection(config.lintspec.skip_version_detection)
.build();
let diagnostics = paths
.par_iter()
.filter_map(|p| {
lint(
parser.clone(),
p,
&options,
!config.output.compact && !config.output.json,
)
.transpose()
})
.collect::<Result<Vec<_>, _>>()?;
let mut output_file: Box<dyn std::io::Write> = match &config.output.out {
Some(path) => {
let _ = miette::set_hook(Box::new(|_| {
Box::new(
miette::MietteHandlerOpts::new()
.terminal_links(false)
.unicode(false)
.color(false)
.build(),
)
}));
Box::new(
File::options()
.truncate(true)
.create(true)
.write(true)
.open(path)
.map_err(|err| crate::error::Error::IOError {
path: path.clone(),
err,
})?,
)
}
None => {
if diagnostics.is_empty() {
Box::new(std::io::stdout())
} else {
Box::new(std::io::stderr())
}
}
};
if diagnostics.is_empty() {
if config.output.json {
writeln!(&mut output_file, "[]")?;
} else {
writeln!(&mut output_file, "No issue found")?;
}
return Ok(RunResult::NoDiagnostics);
}
if config.output.json {
if config.output.compact {
writeln!(&mut output_file, "{}", serde_json::to_string(&diagnostics)?)?;
} else {
writeln!(
&mut output_file,
"{}",
serde_json::to_string_pretty(&diagnostics)?
)?;
}
} else {
let cwd = dunce::canonicalize(env::current_dir()?)?;
let mut contents = if cfg!(any(feature = "slang", feature = "solar")) {
parser.get_sources()?
} else {
HashMap::default()
};
for file_diags in diagnostics {
let source = contents.remove(&file_diags.document_id).unwrap_or_default();
print_reports(
&mut output_file,
&cwd,
file_diags,
source,
config.output.compact,
)?;
}
}
Ok(RunResult::SomeDiagnostics)
}
pub fn write_default_config() -> Result<PathBuf, Box<dyn Error>> {
let config = Config::default();
let path = PathBuf::from(".lintspec.toml");
if path.exists() {
fs::rename(&path, ".lintspec.bck.toml")?;
println!("Existing `.lintspec.toml` file was renamed to `.lintpsec.bck.toml`");
}
fs::write(&path, toml::to_string(&config)?)?;
Ok(dunce::canonicalize(&path)?)
}
pub fn print_reports(
f: &mut impl io::Write,
root_path: impl AsRef<Path>,
file_diags: FileDiagnostics,
contents: String,
compact: bool,
) -> Result<(), io::Error> {
fn inner(
f: &mut impl io::Write,
root_path: &Path,
file_diags: FileDiagnostics,
contents: String,
compact: bool,
) -> Result<(), io::Error> {
if compact {
for item_diags in file_diags.items {
item_diags.print_compact(f, &file_diags.path, root_path)?;
}
} else {
let source_name = match file_diags.path.strip_prefix(root_path) {
Ok(relative_path) => relative_path.to_string_lossy(),
Err(_) => file_diags.path.to_string_lossy(),
};
let source = Arc::new(NamedSource::new(source_name, contents));
for item_diags in file_diags.items {
print_report(f, Arc::clone(&source), item_diags)?;
}
}
Ok(())
}
inner(f, root_path.as_ref(), file_diags, contents, compact)
}
fn print_report(
f: &mut impl io::Write,
source: Arc<NamedSource<String>>,
item: ItemDiagnostics,
) -> Result<(), io::Error> {
let msg = if let Some(parent) = &item.parent {
format!("{} {}.{}", item.item_type, parent, item.name)
} else {
format!("{} {}", item.item_type, item.name)
};
let labels: Vec<_> = item
.diags
.into_iter()
.map(|d| {
LabeledSpan::new(
Some(d.message),
d.span.start.utf8,
d.span.end.utf8 - d.span.start.utf8,
)
})
.collect();
let report: miette::Report = MietteDiagnostic::new(msg).with_labels(labels).into();
write!(f, "{:?}", report.with_source_code(source))
}