Skip to content

Commit

Permalink
Add global --log-level and --no-color CLI options and logging sup…
Browse files Browse the repository at this point in the history
…port (#155)

* Move `AIR_CRATE_NAMES` to its own crate

* Add global `--log-level` and `--no-color` CLI options

Along with tracing-subscriber support in the CLI

* CHANGELOG bullets
  • Loading branch information
DavisVaughan authored Jan 17, 2025
1 parent cb5d577 commit 061b4db
Show file tree
Hide file tree
Showing 18 changed files with 229 additions and 44 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

# Development version

- In the CLI, errors and warnings are now written to stderr. This allows you to
see issues that occur during `air format`, such as parse errors or file not
found errors (#155).

- New global CLI option `--log-level` to control the log level. The default is
`warn` (#155).

- New global CLI option `--no-color` to disable colored output (#155).

- Air now supports `.air.toml` files in addition to `air.toml` files. If both
are in the same directory, `air.toml` is preferred, but we don't recommend
doing that (#152).
Expand Down
24 changes: 21 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ air_r_formatter = { path = "./crates/air_r_formatter" }
air_r_parser = { path = "./crates/air_r_parser" }
air_r_syntax = { path = "./crates/air_r_syntax" }
biome_ungrammar = { path = "./crates/biome_ungrammar" }
crates = { path = "./crates/crates" }
fs = { path = "./crates/fs" }
line_ending = { path = "./crates/line_ending" }
lsp = { path = "./crates/lsp" }
Expand All @@ -49,6 +50,7 @@ biome_unicode_table = { git = "https://github.com/biomejs/biome", rev = "2648fa4
bytes = "1.8.0"
cargo_metadata = "0.19.1"
clap = { version = "4.5.20", features = ["derive"] }
colored = "3.0.0"
crossbeam = "0.8.4"
dissimilar = "1.0.9"
futures = "0.3.31"
Expand Down
5 changes: 3 additions & 2 deletions crates/air/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ rust-version.workspace = true
air_r_formatter = { workspace = true }
air_r_parser = { workspace = true }
anyhow = { workspace = true }
biome_console = { workspace = true }
biome_diagnostics = { workspace = true }
biome_formatter = { workspace = true }
biome_parser = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }
colored = { workspace = true }
crates = { workspace = true }
fs = { workspace = true }
ignore = { workspace = true }
itertools = { workspace = true }
Expand All @@ -28,6 +28,7 @@ lsp = { workspace = true }
thiserror = { workspace = true }
tokio = "1.41.1"
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
workspace = { workspace = true }

[dev-dependencies]
Expand Down
20 changes: 20 additions & 0 deletions crates/air/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use clap::Parser;
use clap::Subcommand;
use std::path::PathBuf;

use crate::logging;

#[derive(Parser)]
#[command(
author,
Expand All @@ -13,6 +15,9 @@ use std::path::PathBuf;
pub struct Args {
#[command(subcommand)]
pub(crate) command: Command,

#[clap(flatten)]
pub(crate) global_options: GlobalOptions,
}

#[derive(Subcommand)]
Expand All @@ -39,3 +44,18 @@ pub(crate) struct FormatCommand {

#[derive(Clone, Debug, Parser)]
pub(crate) struct LanguageServerCommand {}

/// All configuration options that can be passed "globally"
#[derive(Debug, Default, clap::Args)]
#[command(next_help_heading = "Global options")]
pub(crate) struct GlobalOptions {
/// The log level. One of: `error`, `warn`, `info`, `debug`, or `trace`. Defaults
/// to `warn`.
#[arg(long, global = true)]
pub(crate) log_level: Option<logging::LogLevel>,

/// Disable colored output. To turn colored output off, either set this option or set
/// the environment variable `NO_COLOR` to any non-zero value.
#[arg(long, global = true)]
pub(crate) no_color: bool,
}
9 changes: 5 additions & 4 deletions crates/air/src/commands/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::path::PathBuf;

use air_r_formatter::context::RFormatOptions;
use air_r_parser::RParserOptions;
use colored::Colorize;
use fs::relativize_path;
use itertools::Either;
use itertools::Itertools;
Expand Down Expand Up @@ -215,7 +216,7 @@ impl Display for FormatCommandError {
write!(
f,
"Failed to format {path}: {err}",
path = relativize_path(path),
path = relativize_path(path).underline(),
err = err
.io_error()
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
Expand All @@ -234,21 +235,21 @@ impl Display for FormatCommandError {
write!(
f,
"Failed to read {path}: {err}",
path = relativize_path(path),
path = relativize_path(path).underline(),
)
}
Self::Write(path, err) => {
write!(
f,
"Failed to write {path}: {err}",
path = relativize_path(path),
path = relativize_path(path).underline(),
)
}
Self::Format(path, err) => {
write!(
f,
"Failed to format {path}: {err}",
path = relativize_path(path),
path = relativize_path(path).underline(),
)
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/air/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ use crate::status::ExitStatus;

pub mod args;
mod commands;
mod logging;
pub mod status;

pub fn run(args: Args) -> anyhow::Result<ExitStatus> {
logging::init_logging(
args.global_options.log_level.unwrap_or_default(),
args.global_options.no_color,
);

match args.command {
Command::Format(command) => commands::format::format(command),
Command::LanguageServer(command) => commands::language_server::language_server(command),
Expand Down
108 changes: 108 additions & 0 deletions crates/air/src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::fmt::Display;
use std::str::FromStr;
use tracing_subscriber::filter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Layer;
use tracing_subscriber::Registry;

pub(crate) fn init_logging(log_level: LogLevel, no_color: bool) {
let log_level = log_level.tracing_level();

// Apply the log level to each air crate.
// We don't report any logs from non-air crates in the CLI.
let mut filter = filter::Targets::new();
for target in crates::AIR_CRATE_NAMES {
filter = filter.with_target(*target, log_level);
}

let mut layer = tracing_subscriber::fmt::layer();

if no_color {
layer = turn_off_colors(layer);
};

let layer = layer
// i.e. Displaying `ERROR` or `WARN`
.with_level(true)
// Don't show the module name, not useful in a cli
.with_target(false)
// Don't show the timestamp, not useful in a cli
.without_time()
.with_writer(std::io::stderr)
.with_filter(filter);

let subscriber = tracing_subscriber::Registry::default().with(layer);

tracing::subscriber::set_global_default(subscriber)
.expect("Should be able to set the global subscriber exactly once.");

// Emit message after subscriber is set up, so we actually see it
tracing::trace!("Initialized logging");
}

/// Explicitly turn off ANSI colored / styled output
///
/// We use colored output in two places:
/// - Level labels in tracing-subscriber, like `ERROR` , which shows up in red
/// - Usage of the `colored` crate
///
/// Both respect the `NO_COLOR` environment variable if we don't specify anything.
///
/// We explicitly call `set_override()` and `with_ansi()` ONLY when turning colors off.
/// `set_override(true)` and `with_ansi(true)` override the `NO_COLOR` environment
/// variable, and we don't want to do that.
fn turn_off_colors(
layer: tracing_subscriber::fmt::Layer<Registry>,
) -> tracing_subscriber::fmt::Layer<Registry> {
colored::control::set_override(false);
layer.with_ansi(false)
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum LogLevel {
Error,
#[default]
Warn,
Info,
Debug,
Trace,
}

impl LogLevel {
fn tracing_level(self) -> tracing::Level {
match self {
Self::Error => tracing::Level::ERROR,
Self::Warn => tracing::Level::WARN,
Self::Info => tracing::Level::INFO,
Self::Debug => tracing::Level::DEBUG,
Self::Trace => tracing::Level::TRACE,
}
}
}

impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Error => f.write_str("Error"),
Self::Warn => f.write_str("Warn"),
Self::Info => f.write_str("Info"),
Self::Debug => f.write_str("Debug"),
Self::Trace => f.write_str("Trace"),
}
}
}

impl FromStr for LogLevel {
type Err = anyhow::Error;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
value => Err(anyhow::anyhow!("Can't parse log level from '{value}'.")),
}
}
}
26 changes: 26 additions & 0 deletions crates/air/tests/format-check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::path::Path;
use std::path::PathBuf;

use air::args::Args;
use air::run;
use air::status::ExitStatus;
use clap::Parser;

#[test]
fn test_check_returns_cleanly_for_multiline_strings_with_crlf_line_endings() -> anyhow::Result<()> {
let fixtures = path_fixtures();
let path = fixtures.join("crlf").join("multiline_string_value.R");
let path = path.to_str().unwrap();

let args = Args::parse_from(["", "format", path, "--check"]);
let err = run(args)?;

assert_eq!(err, ExitStatus::Success);
Ok(())
}

fn path_fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
22 changes: 0 additions & 22 deletions crates/air/tests/format.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
use std::path::Path;
use std::path::PathBuf;

use air::args::Args;
use air::run;
use air::status::ExitStatus;
Expand All @@ -24,22 +21,3 @@ fn default_options() -> anyhow::Result<()> {
assert_eq!(err, ExitStatus::Success);
Ok(())
}

#[test]
fn test_check_returns_cleanly_for_multiline_strings_with_crlf_line_endings() -> anyhow::Result<()> {
let fixtures = path_fixtures();
let path = fixtures.join("crlf").join("multiline_string_value.R");
let path = path.to_str().unwrap();

let args = Args::parse_from(["", "format", path, "--check"]);
let err = run(args)?;

assert_eq!(err, ExitStatus::Success);
Ok(())
}

fn path_fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
2 changes: 1 addition & 1 deletion crates/air_r_parser/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ fn parse_failure() -> (Vec<Event<RSyntaxKind>>, Vec<Trivia>, Vec<ParseError>) {

// Generate a single diagnostic, wrap it in our error type
let span: Option<TextRange> = None;
let diagnostic = ParseDiagnostic::new("Failed to parse", span);
let diagnostic = ParseDiagnostic::new("Failed to parse due to syntax errors.", span);
let error = ParseError::from(diagnostic);
let errors = vec![error];

Expand Down
Loading

0 comments on commit 061b4db

Please sign in to comment.