diff --git a/Cargo.lock b/Cargo.lock index 8fcefa97489..a629bc9fa9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fnv" version = "1.0.7" @@ -307,6 +313,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + [[package]] name = "heck" version = "0.4.0" @@ -338,7 +350,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", ] [[package]] @@ -394,9 +416,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "nu-ansi-term" @@ -542,6 +564,7 @@ dependencies = [ "term", "thiserror", "toml", + "toml_edit 0.20.4", "tracing", "tracing-subscriber", "unicode-properties", @@ -700,14 +723,14 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.10", ] [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -718,11 +741,22 @@ version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" dependencies = [ - "indexmap", + "indexmap 1.9.3", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.4.7", +] + +[[package]] +name = "toml_edit" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380f9e8120405471f7c9ad1860a713ef5ece6a670c7eae39225e477340f32fc4" +dependencies = [ + "indexmap 2.0.2", + "toml_datetime", + "winnow 0.5.17", ] [[package]] @@ -946,6 +980,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +dependencies = [ + "memchr", +] + [[package]] name = "yansi-term" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 00e0ed37a84..4822f203d9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ thiserror = "1.0.40" toml = "0.7.4" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +toml_edit = "0.20.4" unicode-segmentation = "1.9" unicode-width = "0.1" unicode-properties = { version = "0.1", default-features = false, features = ["general-category"] } diff --git a/Configurations.md b/Configurations.md index ac5747800b2..2e1e50d0362 100644 --- a/Configurations.md +++ b/Configurations.md @@ -1040,6 +1040,44 @@ fn add_one(x: i32) -> i32 { } ``` +## `format_cargo_toml` + +Format `Cargo.toml` files according to the [Cargo.toml conventions](https://github.com/rust-dev-tools/fmt-rfcs/blob/master/guide/cargo.md). + +- **Default value**: `false` +- **Possible values**: `true`, `false` +- **Stable**: No (tracking issue: [#4091](https://github.com/rust-lang/rustfmt/issues/4091)) + +#### `false` (default): + +```toml + [package] +edition="2018" + +version="0.1.0" + name="e" +[dependencies] +a="0.1" + + +c="0.3" +b="0.2" +``` + +#### `true`: + +```toml +[package] +name = "e" +version = "0.1.0" +edition = "2018" + +[dependencies] +a = "0.1" +b = "0.2" +c = "0.3" +``` + ## `doc_comment_code_block_width` Max width for code snippets included in doc comments. Only used if [`format_code_in_doc_comments`](#format_code_in_doc_comments) is true. diff --git a/src/cargo-fmt/main.rs b/src/cargo-fmt/main.rs index a1ad1aafac4..d4b164829af 100644 --- a/src/cargo-fmt/main.rs +++ b/src/cargo-fmt/main.rs @@ -285,6 +285,15 @@ impl Target { edition: target.edition, } } + + /// `Cargo.toml` file to format. + pub fn manifest_target(manifest_path: PathBuf) -> Self { + Target { + path: manifest_path, + kind: String::from("manifest"), + edition: Edition::E2021, // The value doesn't matter for formatting Cargo.toml. + } + } } impl PartialEq for Target { @@ -366,6 +375,9 @@ fn get_targets_root_only( ) -> Result<(), io::Error> { let metadata = get_cargo_metadata(manifest_path)?; let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?; + targets.insert(Target::manifest_target( + workspace_root_path.join("Cargo.toml"), + )); let (in_workspace_root, current_dir_manifest) = if let Some(target_manifest) = manifest_path { ( workspace_root_path == target_manifest, @@ -380,7 +392,15 @@ fn get_targets_root_only( }; let package_targets = match metadata.packages.len() { - 1 => metadata.packages.into_iter().next().unwrap().targets, + 1 => { + let p = metadata.packages.into_iter().next().unwrap(); + targets.insert(Target::manifest_target( + PathBuf::from(&p.manifest_path) + .canonicalize() + .unwrap_or_default(), + )); + p.targets + } _ => metadata .packages .into_iter() @@ -391,13 +411,18 @@ fn get_targets_root_only( .unwrap_or_default() == current_dir_manifest }) - .flat_map(|p| p.targets) + .flat_map(|p| { + targets.insert(Target::manifest_target( + PathBuf::from(&p.manifest_path) + .canonicalize() + .unwrap_or_default(), + )); + p.targets + }) .collect(), }; - for target in package_targets { - targets.insert(Target::from_target(&target)); - } + add_targets(&package_targets, targets); Ok(()) } @@ -409,6 +434,11 @@ fn get_targets_recursive( ) -> Result<(), io::Error> { let metadata = get_cargo_metadata(manifest_path)?; for package in &metadata.packages { + targets.insert(Target::manifest_target( + PathBuf::from(&package.manifest_path) + .canonicalize() + .unwrap_or_default(), + )); add_targets(&package.targets, targets); // Look for local dependencies using information available since cargo v1.51 @@ -448,6 +478,11 @@ fn get_targets_with_hitlist( for package in metadata.packages { if workspace_hitlist.remove(&package.name) { + targets.insert(Target::manifest_target( + PathBuf::from(&package.manifest_path) + .canonicalize() + .unwrap_or_default(), + )); for target in package.targets { targets.insert(Target::from_target(&target)); } diff --git a/src/cargo-fmt/test/targets.rs b/src/cargo-fmt/test/targets.rs index 34accb2136a..e99f1fdb8d8 100644 --- a/src/cargo-fmt/test/targets.rs +++ b/src/cargo-fmt/test/targets.rs @@ -57,7 +57,7 @@ mod all_targets { manifest_suffix, "divergent-crate-dir-names", &exp_targets, - 3, + 3 + 3, // include 3 Cargo.toml files ); } @@ -112,7 +112,7 @@ mod all_targets { manifest_suffix, "workspaces/path-dep-above", &exp_targets, - 6, + 6 + 6, // include 6 Cargo.toml files, ); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 7538b26522d..f1704db5c80 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -176,6 +176,7 @@ create_config! { or they are left with trailing whitespaces"; ignore: IgnoreList, IgnoreList::default(), false, "Skip formatting the specified files and directories"; + format_cargo_toml: bool, false, false, "Format Cargo.toml files"; // Not user-facing verbose: Verbosity, Verbosity::Normal, false, "How much to information to emit to the user"; @@ -685,6 +686,7 @@ hide_parse_errors = false error_on_line_overflow = false error_on_unformatted = false ignore = [] +format_cargo_toml = false emit_mode = "Files" make_backup = false "#, diff --git a/src/formatting.rs b/src/formatting.rs index cd57a025b67..82ef73f4c6d 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -1,7 +1,9 @@ // High level formatting functions. use std::collections::HashMap; +use std::ffi::OsStr; use std::io::{self, Write}; +use std::path::PathBuf; use std::time::{Duration, Instant}; use rustc_ast::ast; @@ -11,6 +13,7 @@ use self::newline_style::apply_newline_style; use crate::comment::{CharClasses, FullCodeCharKind}; use crate::config::{Config, FileName, Verbosity}; use crate::formatting::generated::is_generated_file; +use crate::ignore_path::IgnorePathSet; use crate::modules::Module; use crate::parse::parser::{DirectoryOwnership, Parser, ParserError}; use crate::parse::session::ParseSess; @@ -18,6 +21,7 @@ use crate::utils::{contains_skip, count_newlines}; use crate::visitor::FmtVisitor; use crate::{modules, source_file, ErrorKind, FormatReport, Input, Session}; +mod cargo_toml; mod generated; mod newline_style; @@ -35,6 +39,15 @@ impl<'b, T: Write + 'b> Session<'b, T> { return Err(ErrorKind::VersionMismatch); } + let cargo_toml = Some(OsStr::new("Cargo.toml")); + match input { + Input::File(path) if path.file_name() == cargo_toml => { + let config = &self.config.clone(); + return format_cargo_toml(path, config, self); + } + _ => {} + } + rustc_span::create_session_if_not_set_then(self.config.edition().into(), |_| { if self.config.disable_all_formatting() { // When the input is from stdin, echo back the input. @@ -174,6 +187,29 @@ fn format_project( Ok(context.report) } +fn format_cargo_toml( + path: PathBuf, + config: &Config, + handler: &mut T, +) -> Result { + let mut report = FormatReport::new(); + + let ignore_path_set = IgnorePathSet::from_ignore_list(&config.ignore())?; + let file_name = FileName::Real(path.clone()); + if !config.format_cargo_toml() || ignore_path_set.is_match(&file_name) { + return Ok(report); + } + + let input = std::fs::read_to_string(&path)?; + let mut result = cargo_toml::format_cargo_toml_inner(&input, config)?; + + apply_newline_style(config.newline_style(), &mut result, &input); + + handler.handle_formatted_file(None, file_name, result, &mut report)?; + + Ok(report) +} + // Used for formatting files. struct FormatContext<'a, T: FormatHandler> { krate: &'a ast::Crate, @@ -256,7 +292,7 @@ impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> { .add_non_formatted_ranges(visitor.skipped_range.borrow().clone()); self.handler.handle_formatted_file( - &self.parse_session, + Some(&self.parse_session), path, visitor.buffer.to_owned(), &mut self.report, @@ -268,7 +304,7 @@ impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> { trait FormatHandler { fn handle_formatted_file( &mut self, - parse_session: &ParseSess, + parse_session: Option<&ParseSess>, path: FileName, result: String, report: &mut FormatReport, @@ -279,14 +315,14 @@ impl<'b, T: Write + 'b> FormatHandler for Session<'b, T> { // Called for each formatted file. fn handle_formatted_file( &mut self, - parse_session: &ParseSess, + parse_session: Option<&ParseSess>, path: FileName, result: String, report: &mut FormatReport, ) -> Result<(), ErrorKind> { if let Some(ref mut out) = self.out { match source_file::write_file( - Some(parse_session), + parse_session, &path, &result, out, diff --git a/src/formatting/cargo_toml.rs b/src/formatting/cargo_toml.rs new file mode 100644 index 00000000000..f3b0b0b749b --- /dev/null +++ b/src/formatting/cargo_toml.rs @@ -0,0 +1,476 @@ +use itertools::Itertools; +use std::cmp::Ordering; +use toml_edit::{ + visit_mut::*, Decor, Document, Formatted, Item, KeyMut, RawString, Table, TableLike, TomlError, + Value, +}; + +use crate::{Config, ErrorKind}; + +/// Format `Cargo.toml` according to [the Style Guide]. +/// +/// [the Style Guide]: https://github.com/rust-dev-tools/fmt-rfcs/blob/master/guide/cargo.md +pub(crate) fn format_cargo_toml_inner(content: &str, config: &Config) -> Result { + let mut doc = content.parse::()?; + let rules = [ + &mut SortSection { + current_position: 0, + } as &mut dyn VisitMut, + &mut BlankLine { trimming: true }, + &mut KeyValue, + &mut MultiLine, + &mut WrapArray { + max_width: config.max_width(), + }, + &mut FormatInlineTable { + max_width: config.max_width(), + long_tables: vec![], + current_section: vec![], + }, + &mut TrimSpaces, + ]; + for rule in rules.into_iter() { + rule.visit_document_mut(&mut doc); + } + // Special handling for fallible rules. + let mut rule = SortKey { error: None }; + rule.visit_document_mut(&mut doc); + if let Some(e) = rule.error { + return Err(e); + } + + Ok(doc.to_string()) +} + +impl From for ErrorKind { + fn from(_: TomlError) -> Self { + ErrorKind::ParseError + } +} + +/// Sort key names alphabetically within each section, with the exception of the +/// `[package]` section. +/// +/// In `[package]` section, +/// put the `name` and `version` keys in that order at the top of that section, +/// followed by the remaining keys other than `description` in alphabetical order, +/// followed by the `description` at the end of that section. +struct SortKey { + error: Option, +} + +/// Put the `[package]` section at the top of the file +struct SortSection { + /// `cargo-edit` uses a `position` field to put tables back in their original order when + /// serialising. We should reset this field after sorting. + current_position: usize, +} + +/// Put a blank line between the last key-value pair in a section and the header of the next +/// section. +/// +/// Do not place a blank line between section headers and the key-value pairs in that section, or +/// between key-value pairs in a section. +/// +/// Should be applied after `SortSection`. +struct BlankLine { + trimming: bool, +} + +/// Trim unnecessary spaces. +/// +/// Note: this is not included in the Style Guide. +struct TrimSpaces; + +/// Don't use quotes around any standard key names; use bare keys. Only use quoted +/// keys for non-standard keys whose names require them, and avoid introducing such +/// key names when possible. +/// +/// Put a single space both before and after the = between a key and value. +/// Do not indent any key names; start all key names at the start of a line. +struct KeyValue; + +/// Use multi-line strings (rather than newline escape sequences) for any string values +/// that include multiple lines, such as the crate description. +struct MultiLine; + +/// For array values, such as a list of authors, put the entire list on the same line as the key, +/// if it fits. Otherwise, use block indentation: put a newline after the opening square bracket, +/// indent each item by one indentation level, put a comma after each item (including the last), +/// and put the closing square bracket at the start of a line by itself after the last item. +/// +/// ```toml +/// authors = [ +/// "A Uthor ", +/// "Another Author ", +/// ] +///``` +struct WrapArray { + max_width: usize, +} + +/// For table values, such as a crate dependency with a path, write the entire +/// table using curly braces and commas on the same line as the key if it fits. If +/// the entire table does not fit on the same line as the key, separate it out into +/// a separate section with key-value pairs: +/// +/// ```toml +/// [dependencies] +/// crate1 = { path = "crate1", version = "1.2.3" } +/// +/// [dependencies.extremely_long_crate_name_goes_here] +/// path = "extremely_long_path_name_goes_right_here" +/// version = "4.5.6" +/// ``` +struct FormatInlineTable { + max_width: usize, + /// Must be `InlineTable` + long_tables: Vec<(Vec, String, Item)>, + current_section: Vec, +} + +impl VisitMut for SortKey { + fn visit_document_mut(&mut self, doc: &mut Document) { + doc.as_table_mut().iter_mut().for_each(|(key, section)| { + if key == "package" { + let table = match section.as_table_mut() { + Some(table) => table, + None => { + // package should be a table + self.error = Some(ErrorKind::ParseError); + return; + } + }; + // "name" is the first, "version" is the second, "description" is the last + // everything else is sorted alphabetically + table.sort_values_by(|k1, _, k2, _| match (k1.get(), k2.get()) { + ("name", _) => Ordering::Less, + (_, "name") => Ordering::Greater, + ("version", _) => Ordering::Less, + (_, "version") => Ordering::Greater, + ("description", _) => Ordering::Greater, + (_, "description") => Ordering::Less, + _ => k1.cmp(k2), + }) + } else { + self.visit_item_mut(section) + } + }); + } + + fn visit_table_like_mut(&mut self, table: &mut dyn TableLike) { + table.sort_values(); + } +} + +impl BlankLine { + /// trim blank lines at the beginning and end + fn trim_blank_lines(s: &str) -> String { + if !s.contains('\n') { + return s.to_string(); + } + + let num_lines = s.lines().count(); + + if let Some((first_line, _)) = s.lines().find_position(|line| !line.trim().is_empty()) { + // last_line may be equal to first_line + let (mut last_line, _) = s + .lines() + .rev() + .find_position(|line| !line.trim().is_empty()) + .unwrap(); + last_line = num_lines - last_line; + s.lines() + .skip(first_line) + .take(last_line - first_line) + .join("\n") + + "\n" + } else { + String::new() + } + } + + fn trim_decor_blank_lines(decor: &mut Decor) { + if let Some(prefix) = decor.prefix().map(raw_string_as_str) { + decor.set_prefix(Self::trim_blank_lines(prefix)); + } + if let Some(suffix) = decor.suffix().map(raw_string_as_str) { + decor.set_suffix(Self::trim_blank_lines(suffix)); + } + } +} + +impl VisitMut for BlankLine { + fn visit_document_mut(&mut self, doc: &mut Document) { + doc.as_table_mut() + .iter_mut() + .for_each(|(mut key, section)| { + Self::trim_decor_blank_lines(key.decor_mut()); + self.visit_item_mut(section); + }); + + self.trimming = false; + doc.as_table_mut() + .iter_mut() + .skip(1) + .for_each(|(_, section)| self.visit_item_mut(section)) + } + + fn visit_table_mut(&mut self, table: &mut Table) { + if self.trimming { + Self::trim_decor_blank_lines(table.decor_mut()); + table.iter_mut().for_each(|(mut key, _)| { + Self::trim_decor_blank_lines(key.decor_mut()); + }); + } else { + let decor = table.decor_mut(); + decor.set_prefix(format!( + "\n{}", + decor.prefix().map(raw_string_as_str).unwrap_or_default() + )); + } + } +} + +impl KeyValue { + /// Bare keys can contain ASCII letters, ASCII digits, underscores, and dashes `(A-Za-z0-9_-)`. + fn can_be_bare_key(key: &str) -> bool { + key.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + } +} + +impl VisitMut for KeyValue { + fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, value: &mut Item) { + let original_prefix = key + .decor() + .prefix() + .map(raw_string_as_str) + .map(String::from); + if Self::can_be_bare_key(key.get()) { + // will remove decors and set the key to the bare key + key.fmt(); + } else { + // add a space after the key + key.decor_mut().set_suffix(" "); + } + // start all key names at the start of a line, but preserve comments + if let Some(prefix) = original_prefix { + key.decor_mut() + .set_prefix(prefix.trim_end_matches(|c: char| c.is_whitespace() && c != '\n')); + } + + if let Some(v) = value.as_value_mut() { + v.decor_mut().set_prefix(" "); + } + + self.visit_item_mut(value); + } +} + +impl VisitMut for SortSection { + fn visit_document_mut(&mut self, doc: &mut Document) { + // put package at the beginning, others unchanged + doc.as_table_mut().sort_values_by(|k1, _, k2, _| { + if k1.get() == "package" { + Ordering::Less + } else if k2.get() == "package" { + Ordering::Greater + } else { + Ordering::Equal + } + }); + + doc.as_table_mut().iter_mut().for_each(|(_, section)| { + self.visit_item_mut(section); + }); + } + + fn visit_table_mut(&mut self, table: &mut Table) { + table.set_position(self.current_position); + self.current_position += 1; + for (_, v) in table.iter_mut().sorted_by_key(|(k, _)| k.to_string()) { + self.visit_item_mut(v); + } + } +} + +impl VisitMut for MultiLine { + fn visit_string_mut(&mut self, s: &mut Formatted) { + s.fmt(); + } +} + +impl VisitMut for WrapArray { + fn visit_table_like_kv_mut(&mut self, key: KeyMut<'_>, node: &mut Item) { + if let Some(array) = node.as_array_mut() { + // Format to [item1, item2, ...] + array.fmt(); + // Length of key doesn't include decor. Length of array does. So we add 2 (" ="). + if key.get().len() + 2 + array.to_string().len() > self.max_width { + array.iter_mut().for_each(|item| { + item.decor_mut().set_prefix("\n "); + }); + array + .iter_mut() + .last() + .unwrap() + .decor_mut() + .set_suffix("\n"); + } + } + self.visit_item_mut(node); + } +} + +impl VisitMut for FormatInlineTable { + fn visit_document_mut(&mut self, doc: &mut Document) { + doc.as_table_mut().iter_mut().for_each(|(key, section)| { + self.current_section = vec![key.to_owned()]; + self.visit_table_like_kv_mut(key, section); + }); + + let mut long_tables = vec![]; + std::mem::swap(&mut self.long_tables, &mut long_tables); + + long_tables + .into_iter() + .for_each(|(sections, key, table)| match table { + Item::Value(Value::InlineTable(table)) => { + let mut section = doc.as_item_mut(); + for key in sections { + section = &mut section[&key] + } + section[&key] = Item::Table(table.into_table()); + } + _ => unreachable!(), + }); + } + + fn visit_table_like_mut(&mut self, table: &mut dyn TableLike) { + let mut long_table_keys = vec![]; + + table.iter_mut().for_each(|(key, node)| { + if let Some(table) = node.as_inline_table_mut() { + // Format to { k1 = v1, k2 = v2, ...} + table.fmt(); + // Length of key doesn't include decor. Length of array does. So we add 2 (" ="). + if key.get().len() + 2 + table.to_string().len() > self.max_width { + long_table_keys.push(key.get().to_owned()); + } + } + }); + + long_table_keys.into_iter().sorted().for_each(|key| { + let item = table.remove(&key).unwrap(); + self.long_tables + .push((self.current_section.clone(), key, item)); + }); + + table.iter_mut().for_each(|(key, node)| { + self.current_section.push(key.to_owned()); + self.visit_item_mut(node); + self.current_section.pop(); + }); + } +} + +impl TrimSpaces { + fn trim_block(s: &str) -> String { + let s = s.trim(); + if s.is_empty() { + return String::new(); + } + + let s: String = s + .lines() + .into_iter() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.is_empty() { + None + } else { + Some(format!("{trimmed}")) + } + }) + .join("\n"); + + format!("{}\n", s) + } + + fn trim_suffix(s: &str) -> String { + let s = s.trim(); + if s.is_empty() { + String::new() + } else { + format!(" {}", s) + } + } +} + +impl VisitMut for TrimSpaces { + fn visit_document_mut(&mut self, node: &mut Document) { + self.visit_table_mut(node); + + let set_prefix = |decor: &mut Decor, i: usize| { + if let Some(prefix) = decor.prefix().map(raw_string_as_str) { + let prefix = format!( + "{}{}", + if i == 0 { "" } else { "\n" }, + Self::trim_block(prefix) + ); + decor.set_prefix(prefix); + } + }; + let table = node.as_table_mut(); + for (i, (_, item)) in table.iter_mut().enumerate() { + if let Some(table) = item.as_table_mut() { + set_prefix(table.decor_mut(), i); + } else if let Some(arr) = item.as_array_of_tables_mut() { + for table in arr.iter_mut() { + set_prefix(table.decor_mut(), i); + } + } + } + + let trailing = raw_string_as_str(node.trailing()); + if !trailing.trim().is_empty() { + let trailing = Self::trim_block(trailing); + node.set_trailing(&format!("\n{trailing}")); + } else { + node.set_trailing(""); + } + } + + fn visit_table_mut(&mut self, node: &mut Table) { + let decor = node.decor_mut(); + if let Some(prefix) = decor.prefix().map(raw_string_as_str) { + decor.set_prefix(format!("\n{}", Self::trim_block(prefix))); + } + if let Some(suffix) = decor.suffix().map(raw_string_as_str) { + decor.set_suffix(Self::trim_suffix(suffix)); + } + self.visit_table_like_mut(node); + } + + fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, value: &mut Item) { + let decor = key.decor_mut(); + if let Some(prefix) = decor.prefix().map(raw_string_as_str) { + decor.set_prefix(format!("{}", Self::trim_block(prefix))); + } + + if let Some(value) = value.as_value_mut() { + let decor = value.decor_mut(); + if let Some(suffix) = decor.suffix().map(raw_string_as_str) { + decor.set_suffix(Self::trim_suffix(suffix)); + } + } + self.visit_item_mut(value); + } +} + +/// Note: in `Document::from_str`, the document is despanned, so we can safely unwrap `as_str` +/// when handling `RawString`. +fn raw_string_as_str(raw_string: &RawString) -> &str { + raw_string.as_str().expect("should already be despanded") +} diff --git a/src/ignore_path.rs b/src/ignore_path.rs index d955949496a..609505258bd 100644 --- a/src/ignore_path.rs +++ b/src/ignore_path.rs @@ -1,6 +1,7 @@ use ignore::{self, gitignore}; use crate::config::{FileName, IgnoreList}; +use crate::ErrorKind; pub(crate) struct IgnorePathSet { ignore_set: gitignore::Gitignore, @@ -30,6 +31,12 @@ impl IgnorePathSet { } } +impl From for ErrorKind { + fn from(error: ignore::Error) -> Self { + ErrorKind::InvalidGlobPattern(error) + } +} + #[cfg(test)] mod test { use rustfmt_config_proc_macro::nightly_only_test; diff --git a/src/parse/session.rs b/src/parse/session.rs index 0573df9de2f..70baeef43ea 100644 --- a/src/parse/session.rs +++ b/src/parse/session.rs @@ -152,10 +152,7 @@ fn default_handler( impl ParseSess { pub(crate) fn new(config: &Config) -> Result { - let ignore_path_set = match IgnorePathSet::from_ignore_list(&config.ignore()) { - Ok(ignore_path_set) => Lrc::new(ignore_path_set), - Err(e) => return Err(ErrorKind::InvalidGlobPattern(e)), - }; + let ignore_path_set = Lrc::new(IgnorePathSet::from_ignore_list(&config.ignore())?); let source_map = Lrc::new(SourceMap::new(FilePathMapping::empty())); let can_reset_errors = Lrc::new(AtomicBool::new(false)); diff --git a/src/test/mod.rs b/src/test/mod.rs index 47f89c1871a..176e9921b6f 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -95,8 +95,8 @@ fn is_file_skip(path: &Path) -> bool { .any(|file_path| is_subpath(path, file_path)) } -// Returns a `Vec` containing `PathBuf`s of files with an `rs` extension in the -// given path. The `recursive` argument controls if files from subdirectories +// Returns a `Vec` containing `PathBuf`s of files with an `rs` extension or a `Cargo.toml` filename +// in the given path. The `recursive` argument controls if files from subdirectories // are also returned. fn get_test_files(path: &Path, recursive: bool) -> Vec { let mut files = vec![]; @@ -109,7 +109,10 @@ fn get_test_files(path: &Path, recursive: bool) -> Vec { let path = entry.path(); if path.is_dir() && recursive { files.append(&mut get_test_files(&path, recursive)); - } else if path.extension().map_or(false, |f| f == "rs") && !is_file_skip(&path) { + } else if (path.extension().map_or(false, |f| f == "rs") + || path.file_name().map_or(false, |f| f == "Cargo.toml")) + && !is_file_skip(&path) + { files.push(path); } } @@ -781,15 +784,24 @@ fn get_config(config_file: Option<&Path>) -> Config { // Reads significant comments of the form: `// rustfmt-key: value` into a hash map. fn read_significant_comments(file_name: &Path) -> HashMap { + let is_cargo_toml = file_name.file_name().map_or(false, |f| f == "Cargo.toml"); let file = fs::File::open(file_name) .unwrap_or_else(|_| panic!("couldn't read file {}", file_name.display())); let reader = BufReader::new(file); - let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)"; + let pattern = if is_cargo_toml { + r"^\s*#\s*rustfmt-([^:]+):\s*(\S+)" + } else { + r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)" + }; let regex = regex::Regex::new(pattern).expect("failed creating pattern 1"); // Matches lines containing significant comments or whitespace. - let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)") - .expect("failed creating pattern 2"); + let line_regex = regex::Regex::new(if is_cargo_toml { + r"(^\s*$)|(^\s*#\s*rustfmt-[^:]+:\s*\S+)" + } else { + r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)" + }) + .expect("failed creating pattern 2"); reader .lines() diff --git a/tests/source/configs/format_cargo_toml/false/Cargo.toml b/tests/source/configs/format_cargo_toml/false/Cargo.toml new file mode 100644 index 00000000000..a0b4ad1164d --- /dev/null +++ b/tests/source/configs/format_cargo_toml/false/Cargo.toml @@ -0,0 +1,48 @@ +# rustfmt-format_cargo_toml: false + + # comment before [[bin]], not necessarily at the beginning + + + [[bin]] # comment after [[bin]] + "aa" = 1 # comment for "aa" + 'bb' = 2 + "啊"=1 + + # comment before package + +[package] + version = 1 + description = "a\nb\nhaha" + name = 3 + + # comment 1 for arr1 line1 + +# comment 1 for arr1 line 2 + arr1 = [1, + 2,3] + +# comment 2 for arr2 + + arr2 = ["11111111111111111111111111111111111111111111111111111111111111111111111111111111","1111111111111111111111111111111111111111111111111111111111111111111111111111111"] + + # comment before [[dependencies]], not after [package] + + +[dependencies] + extremely_long_crate_name_goes_here = {path = "extremely_long_path_name_goes_right_here",version = "4.5.6"} + + + crate1 = { path = "crate1",version = "1.2.3" } + +# comment before the second [[bin]] + +[[bin]] + d = "git-rustfmt" + c = "src/git-rustfmt/main.rs" + + + + + # comment at the end of the file + + diff --git a/tests/source/configs/format_cargo_toml/true/basic/Cargo.toml b/tests/source/configs/format_cargo_toml/true/basic/Cargo.toml new file mode 100644 index 00000000000..9134c689785 --- /dev/null +++ b/tests/source/configs/format_cargo_toml/true/basic/Cargo.toml @@ -0,0 +1,48 @@ +# rustfmt-format_cargo_toml: true + + # comment before [[bin]], not necessarily at the beginning + + + [[bin]] # comment after [[bin]] + "aa" = 1 # comment for "aa" + 'bb' = 2 + "啊"=1 + + # comment before package + +[package] + version = 1 + description = "a\nb\nhaha" + name = 3 + + # comment 1 for arr1 line1 + +# comment 1 for arr1 line 2 + arr1 = [1, + 2,3] + +# comment 2 for arr2 + + arr2 = ["11111111111111111111111111111111111111111111111111111111111111111111111111111111","1111111111111111111111111111111111111111111111111111111111111111111111111111111"] + + # comment before [[dependencies]], not after [package] + + +[dependencies] + extremely_long_crate_name_goes_here = {path = "extremely_long_path_name_goes_right_here",version = "4.5.6"} + + + crate1 = { path = "crate1",version = "1.2.3" } + +# comment before the second [[bin]] + +[[bin]] + d = "git-rustfmt" + c = "src/git-rustfmt/main.rs" + + + + + # comment at the end of the file + + diff --git a/tests/source/configs/format_cargo_toml/true/dotted-key/Cargo.toml b/tests/source/configs/format_cargo_toml/true/dotted-key/Cargo.toml new file mode 100644 index 00000000000..ecc19768777 --- /dev/null +++ b/tests/source/configs/format_cargo_toml/true/dotted-key/Cargo.toml @@ -0,0 +1,40 @@ +# rustfmt-format_cargo_toml: true + + [a.c] + ac =1 + + # comment for [a.a] + + [a.a] # also comment for [a.a] + aa=1 + + # comment for [a.a.c.d] + +[a.a.c.d] +aacd=1 + +[a.a.b] +veryveryveryveryveryveryveryverylong-table-name2={veryveryveryveryveryveryveryverylong-key-name=2} + aab =1 + +veryveryveryveryveryveryveryverylong-table-name={veryveryveryveryveryveryveryverylong-key-name=1} + +[a.a.c] +aac=1 + + + [a] + +# comment for [a.b], not [a.b.b] +b.b = 1 + + # comment for cc + cc = 3 + + +b.a = 2 + + + [a.z] + az=1 + diff --git a/tests/source/configs/format_cargo_toml/true/virtual-manifest/Cargo.toml b/tests/source/configs/format_cargo_toml/true/virtual-manifest/Cargo.toml new file mode 100644 index 00000000000..c065b63cead --- /dev/null +++ b/tests/source/configs/format_cargo_toml/true/virtual-manifest/Cargo.toml @@ -0,0 +1,39 @@ +# rustfmt-format_cargo_toml: true + + + # Works correctly for Cargo.toml without [package] + + +[workspace] + + +members = ["xtask/", "lib/*", "crates/*"] +exclude = ["crates/proc-macro-test/imp"] + + +[profile.dev] + +# Disabling debug info speeds up builds a bunch, +# and we don't rely on it for debugging that much. + +debug1 = 0 + + +[profile.dev.package] + +# These speed up local tests. +rowan.opt-level = 3 +rustc-hash.opt-level = 3 +smol_str.opt-level = 3 +text-size.opt-level = 3 +# This speeds up `cargo xtask dist`. +miniz_oxide.opt-level = 3 + + +[profile.release] + +incremental = true +# Set this to 1 or 2 to get more useful backtraces in debugger. +debug2 = 0 + + diff --git a/tests/target/configs/format_cargo_toml/false/Cargo.toml b/tests/target/configs/format_cargo_toml/false/Cargo.toml new file mode 100644 index 00000000000..a0b4ad1164d --- /dev/null +++ b/tests/target/configs/format_cargo_toml/false/Cargo.toml @@ -0,0 +1,48 @@ +# rustfmt-format_cargo_toml: false + + # comment before [[bin]], not necessarily at the beginning + + + [[bin]] # comment after [[bin]] + "aa" = 1 # comment for "aa" + 'bb' = 2 + "啊"=1 + + # comment before package + +[package] + version = 1 + description = "a\nb\nhaha" + name = 3 + + # comment 1 for arr1 line1 + +# comment 1 for arr1 line 2 + arr1 = [1, + 2,3] + +# comment 2 for arr2 + + arr2 = ["11111111111111111111111111111111111111111111111111111111111111111111111111111111","1111111111111111111111111111111111111111111111111111111111111111111111111111111"] + + # comment before [[dependencies]], not after [package] + + +[dependencies] + extremely_long_crate_name_goes_here = {path = "extremely_long_path_name_goes_right_here",version = "4.5.6"} + + + crate1 = { path = "crate1",version = "1.2.3" } + +# comment before the second [[bin]] + +[[bin]] + d = "git-rustfmt" + c = "src/git-rustfmt/main.rs" + + + + + # comment at the end of the file + + diff --git a/tests/target/configs/format_cargo_toml/true/basic/Cargo.toml b/tests/target/configs/format_cargo_toml/true/basic/Cargo.toml new file mode 100644 index 00000000000..5733fabc2fb --- /dev/null +++ b/tests/target/configs/format_cargo_toml/true/basic/Cargo.toml @@ -0,0 +1,38 @@ +# comment before package +[package] +name = 3 +version = 1 +# comment 1 for arr1 line1 +# comment 1 for arr1 line 2 +arr1 = [1, 2, 3] +# comment 2 for arr2 +arr2 = [ + "11111111111111111111111111111111111111111111111111111111111111111111111111111111", + "1111111111111111111111111111111111111111111111111111111111111111111111111111111" +] +description = """ +a +b +haha""" + +# rustfmt-format_cargo_toml: true +# comment before [[bin]], not necessarily at the beginning +[[bin]] # comment after [[bin]] +aa = 1 # comment for "aa" +bb = 2 +"啊" = 1 + +# comment before the second [[bin]] +[[bin]] +c = "src/git-rustfmt/main.rs" +d = "git-rustfmt" + +# comment before [[dependencies]], not after [package] +[dependencies] +crate1 = { path = "crate1", version = "1.2.3" } + +[dependencies.extremely_long_crate_name_goes_here] +path = "extremely_long_path_name_goes_right_here" +version = "4.5.6" + +# comment at the end of the file diff --git a/tests/target/configs/format_cargo_toml/true/dotted-key/Cargo.toml b/tests/target/configs/format_cargo_toml/true/dotted-key/Cargo.toml new file mode 100644 index 00000000000..6d7597c7352 --- /dev/null +++ b/tests/target/configs/format_cargo_toml/true/dotted-key/Cargo.toml @@ -0,0 +1,34 @@ +[a] +# comment for [a.b], not [a.b.b] +b.a = 2 +# comment for [a.b], not [a.b.b] +b.b = 1 +# comment for cc +cc = 3 + +# comment for [a.a] +[a.a] # also comment for [a.a] +aa = 1 + +[a.a.b] +aab = 1 + +[a.a.b.veryveryveryveryveryveryveryverylong-table-name] +veryveryveryveryveryveryveryverylong-key-name = 1 + +[a.a.b.veryveryveryveryveryveryveryverylong-table-name2] +veryveryveryveryveryveryveryverylong-key-name = 2 + +[a.a.c] +aac = 1 + +# comment for [a.a.c.d] +[a.a.c.d] +aacd = 1 + +# rustfmt-format_cargo_toml: true +[a.c] +ac = 1 + +[a.z] +az = 1 diff --git a/tests/target/configs/format_cargo_toml/true/virtual-manifest/Cargo.toml b/tests/target/configs/format_cargo_toml/true/virtual-manifest/Cargo.toml new file mode 100644 index 00000000000..4302a93e4e5 --- /dev/null +++ b/tests/target/configs/format_cargo_toml/true/virtual-manifest/Cargo.toml @@ -0,0 +1,24 @@ +# rustfmt-format_cargo_toml: true +# Works correctly for Cargo.toml without [package] +[workspace] +exclude = ["crates/proc-macro-test/imp"] +members = ["xtask/", "lib/*", "crates/*"] + +[profile.dev] +# Disabling debug info speeds up builds a bunch, +# and we don't rely on it for debugging that much. +debug1 = 0 + +[profile.dev.package] +# These speed up local tests. +rowan.opt-level = 3 +rustc-hash.opt-level = 3 +smol_str.opt-level = 3 +text-size.opt-level = 3 +# This speeds up `cargo xtask dist`. +miniz_oxide.opt-level = 3 + +[profile.release] +incremental = true +# Set this to 1 or 2 to get more useful backtraces in debugger. +debug2 = 0