Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add rustfmt support #1902

Merged
merged 11 commits into from
Feb 9, 2024
14 changes: 4 additions & 10 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ dioxus-signals = { path = "packages/signals" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.4.1" }
generational-box = { path = "packages/generational-box", version = "0.4.3" }
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
tracing = "0.1.37"
tracing-futures = "0.2.5"
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fs_extra = "1.2.0"
cargo_toml = "0.18.0"
futures-util = { workspace = true }
notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = { workspace = true }
html_parser = { workspace = true }
cargo_metadata = "0.18.1"
tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
atty = "0.2.14"
Expand All @@ -36,6 +36,7 @@ hyper = "0.14.17"
hyper-rustls = "0.23.2"
indicatif = "0.17.5"
subprocess = "0.2.9"
rayon = "1.8.0"

axum = { version = "0.5.1", features = ["ws", "headers"] }
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
Expand Down Expand Up @@ -74,6 +75,10 @@ toml_edit = "0.21.0"
# bundling
tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }

# formatting
syn = { version = "2.0" }
prettyplease = { workspace = true }

manganis-cli-support = { workspace = true, features = ["webp", "html"] }

dioxus-autofmt = { workspace = true }
Expand All @@ -84,7 +89,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-hot-reload = { workspace = true }
interprocess-docfix = { version = "1.2.2" }
gitignore = "1.0.8"
ignore = "0.4.22"

[features]
default = []
Expand Down
157 changes: 77 additions & 80 deletions packages/cli/src/cli/autoformat.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use dioxus_autofmt::{IndentOptions, IndentType};
use futures_util::{stream::FuturesUnordered, StreamExt};
use rayon::prelude::*;
use std::{fs, path::Path, process::exit};

use super::*;
Expand All @@ -10,6 +10,10 @@ use super::*;
/// Format some rsx
#[derive(Clone, Debug, Parser)]
pub struct Autoformat {
/// Format rust code before the formatting the rsx macros
#[clap(long)]
pub all_code: bool,

/// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits
/// with 1 and prints a diff if formatting is required.
#[clap(short, long)]
Expand All @@ -29,19 +33,19 @@ pub struct Autoformat {
}

impl Autoformat {
// Todo: autoformat the entire crate
pub async fn autoformat(self) -> Result<()> {
pub fn autoformat(self) -> Result<()> {
let Autoformat {
check,
raw,
file,
split_line_attributes,
all_code: format_rust_code,
..
} = self;

// Default to formatting the project
if raw.is_none() && file.is_none() {
if let Err(e) = autoformat_project(check, split_line_attributes).await {
if let Err(e) = autoformat_project(check, split_line_attributes, format_rust_code) {
eprintln!("error formatting project: {}", e);
exit(1);
}
Expand All @@ -60,14 +64,18 @@ impl Autoformat {

// Format single file
if let Some(file) = file {
refactor_file(file, split_line_attributes)?;
refactor_file(file, split_line_attributes, format_rust_code)?;
}

Ok(())
}
}

fn refactor_file(file: String, split_line_attributes: bool) -> Result<(), Error> {
fn refactor_file(
file: String,
split_line_attributes: bool,
format_rust_code: bool,
) -> Result<(), Error> {
let indent = indentation_for(".", split_line_attributes)?;
let file_content = if file == "-" {
let mut contents = String::new();
Expand All @@ -76,10 +84,15 @@ fn refactor_file(file: String, split_line_attributes: bool) -> Result<(), Error>
} else {
fs::read_to_string(&file)
};
let Ok(s) = file_content else {
let Ok(mut s) = file_content else {
eprintln!("failed to open file: {}", file_content.unwrap_err());
exit(1);
};

if format_rust_code {
s = format_rust(&s)?;
}

let edits = dioxus_autofmt::fmt_file(&s, indent);
let out = dioxus_autofmt::apply_formats(&s, edits);

Expand All @@ -94,64 +107,62 @@ fn refactor_file(file: String, split_line_attributes: bool) -> Result<(), Error>
Ok(())
}

fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
let mut files = vec![];

let gitignore_path = config.crate_dir.join(".gitignore");
if gitignore_path.is_file() {
let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
if let Ok(git_files) = gitigno.included_files() {
let git_files = git_files
.into_iter()
.filter(|f| f.ends_with(".rs") && !is_target_dir(f));
files.extend(git_files)
};
} else {
collect_rs_files(&config.crate_dir, &mut files);
use std::ffi::OsStr;
fn get_project_files() -> Vec<PathBuf> {
let mut files = Vec::new();
for result in ignore::Walk::new("./") {
let path = result.unwrap().into_path();
if let Some(ext) = path.extension() {
if ext == OsStr::new("rs") {
files.push(path);
}
}
}

files
}

fn is_target_dir(file: &Path) -> bool {
let stripped = if let Ok(cwd) = std::env::current_dir() {
file.strip_prefix(cwd).unwrap_or(file)
} else {
file
};
if let Some(first) = stripped.components().next() {
first.as_os_str() == "target"
} else {
false
}
}

async fn format_file(
fn format_file(
path: impl AsRef<Path>,
indent: IndentOptions,
) -> Result<usize, tokio::io::Error> {
let contents = tokio::fs::read_to_string(&path).await?;
format_rust_code: bool,
) -> Result<usize> {
let mut contents = fs::read_to_string(&path)?;
let mut if_write = false;
if format_rust_code {
let formatted = format_rust(&contents)
.map_err(|err| Error::ParseError(format!("Syntax Error:\n{}", err)))?;
if contents != formatted {
if_write = true;
contents = formatted;
}
}

let edits = dioxus_autofmt::fmt_file(&contents, indent);
let len = edits.len();

if !edits.is_empty() {
if_write = true;
}

if if_write {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(path, out).await?;
fs::write(path, out)?;
}

Ok(len)
}

/// Read every .rs file accessible when considering the .gitignore and try to format it
///
/// Runs using Tokio for multithreading, so it should be really really fast
/// Runs using rayon for multithreading, so it should be really really fast
///
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
async fn autoformat_project(check: bool, split_line_attributes: bool) -> Result<()> {
let crate_config = dioxus_cli_config::CrateConfig::new(None)?;

let files_to_format = get_project_files(&crate_config);
fn autoformat_project(
check: bool,
split_line_attributes: bool,
format_rust_code: bool,
) -> Result<()> {
let files_to_format = get_project_files();

if files_to_format.is_empty() {
return Ok(());
Expand All @@ -164,26 +175,18 @@ async fn autoformat_project(check: bool, split_line_attributes: bool) -> Result<
let indent = indentation_for(&files_to_format[0], split_line_attributes)?;

let counts = files_to_format
.into_iter()
.map(|path| async {
let path_clone = path.clone();
let res = tokio::spawn(format_file(path, indent.clone())).await;

.into_par_iter()
.map(|path| {
let res = format_file(&path, indent.clone(), format_rust_code);
match res {
Ok(cnt) => Some(cnt),
Err(err) => {
eprintln!("error formatting file: {}\n{err}", path_clone.display());
eprintln!("error formatting file : {}\n{:#?}", path.display(), err);
None
}
Ok(Err(err)) => {
eprintln!("error formatting file: {}\n{err}", path_clone.display());
None
}
Ok(Ok(res)) => Some(res),
}
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
.collect::<Vec<_>>();

let files_formatted: usize = counts.into_iter().flatten().sum();

Expand Down Expand Up @@ -242,28 +245,21 @@ fn indentation_for(
))
}

fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
if is_target_dir(folder.as_ref()) {
return;
}
let Ok(folder) = folder.as_ref().read_dir() else {
return;
};
// load the gitignore
for entry in folder {
let Ok(entry) = entry else {
continue;
};
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, files);
}
if let Some(ext) = path.extension() {
if ext == "rs" && !is_target_dir(&path) {
files.push(path);
}
}
}
/// Format rust code using prettyplease
fn format_rust(input: &str) -> Result<String> {
let syntax_tree = syn::parse_file(input).map_err(format_syn_error)?;
let output = prettyplease::unparse(&syntax_tree);
Ok(output)
}

fn format_syn_error(err: syn::Error) -> Error {
let start = err.span().start();
let line = start.line;
let column = start.column;
Error::ParseError(format!(
"Syntax Error in line {} column {}:\n{}",
line, column, err
))
}

#[tokio::test]
Expand All @@ -284,6 +280,7 @@ async fn test_auto_fmt() {
.to_string();

let fmt = Autoformat {
all_code: false,
check: false,
raw: Some(test_rsx),
file: None,
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ async fn main() -> anyhow::Result<()> {

Autoformat(opts) => opts
.autoformat()
.await
.context(error_wrapper("Error autoformatting RSX")),

Check(opts) => opts
Expand Down