use std::{
fs,
io::{Read, stdin},
path::Path,
};
use crate::{config::Config, format::format_one_file};
use crate::language::Language;
pub fn format_command<W: std::io::Write, P: AsRef<Path>>(
stdout: &mut W,
config: &Config,
filenames: &[P],
write: bool,
language: Option<Language>,
) -> anyhow::Result<()> {
let mut stdin = stdin();
format_command_with_reader(stdout, config, filenames, write, language, &mut stdin)
}
fn format_command_with_reader<W, P, R>(
stdout: &mut W,
config: &Config,
filenames: &[P],
write: bool,
language: Option<Language>,
stdin: &mut R,
) -> anyhow::Result<()>
where
W: std::io::Write,
P: AsRef<Path>,
R: Read,
{
if filenames.is_empty() && !write {
let mut content = String::with_capacity(1024);
stdin.read_to_string(&mut content)?;
format_one_file(stdout, config, language, &content)?;
} else {
for filename in filenames {
let filename = filename.as_ref();
let language = Language::explicit_or_inferred_path(language, filename);
let content = fs::read_to_string(filename)?;
if write {
let mut formatted = Vec::new();
format_one_file(&mut formatted, config, language, &content)?;
fs::write(filename, formatted)?;
} else {
format_one_file(stdout, config, language, &content)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{fs, path::PathBuf};
use tempfile::tempdir;
use super::*;
use crate::config::{Config, SpacingRule};
fn config() -> Config {
let mut config = Config {
max_width: 200,
..Config::default()
};
config.spacing.alphabets = SpacingRule::Require;
config
}
#[test]
fn format_command_applies_spacing_only_to_identified_markdown_files() {
let directory = tempdir().unwrap();
let cases = [
("document.md", "漢 A\n"),
("document.markdown", "漢 A\n"),
("document.MD", "漢 A\n"),
("document.MarkDown", "漢 A\n"),
("document.txt", "漢A\n"),
("document.rs", "漢A\n"),
("document.JSON", "漢A\n"),
("document", "漢A\n"),
("document.md.txt", "漢A\n"),
];
let mut paths = Vec::with_capacity(cases.len());
for (filename, _) in cases {
let path = directory.path().join(filename);
fs::write(&path, "漢A\n").unwrap();
paths.push(path);
}
let mut output = Vec::new();
format_command(&mut output, &config(), &paths, false, None).unwrap();
let expected: String = cases.iter().map(|(_, output)| *output).collect();
assert_eq!(String::from_utf8(output).unwrap(), expected);
for (filename, _) in cases {
assert_eq!(
fs::read_to_string(directory.path().join(filename)).unwrap(),
"漢A\n"
);
}
}
#[test]
fn format_command_writes_the_same_classification_used_for_stdout() {
let directory = tempdir().unwrap();
let cases = [
("document.MarkDown", "漢 A\n"),
("document.JSON", "漢A\n"),
("document.txt", "漢A\n"),
("document", "漢A\n"),
("document.md.txt", "漢A\n"),
];
let paths: Vec<_> = cases
.iter()
.map(|(filename, _)| {
let path = directory.path().join(filename);
fs::write(&path, "漢A\n").unwrap();
path
})
.collect();
let mut output = Vec::new();
format_command(&mut output, &config(), &paths, true, None).unwrap();
assert!(output.is_empty());
for ((_, expected), path) in cases.iter().zip(paths) {
assert_eq!(fs::read_to_string(path).unwrap(), *expected);
}
}
fn assert_all_line_endings_are_crlf(output: &[u8]) {
assert!(
output.windows(2).any(|window| window == b"\r\n"),
"output contains no CRLF line endings: {output:?}"
);
assert!(
!output
.iter()
.enumerate()
.any(|(index, byte)| *byte == b'\n' && (index == 0 || output[index - 1] != b'\r')),
"output contains a bare LF: {output:?}"
);
}
#[test]
fn format_command_write_preserves_crlf_when_wrapping() {
let directory = tempdir().unwrap();
let path = directory.path().join("document.md");
fs::write(&path, "漢A one two three\r\n").unwrap();
let mut config = config();
config.max_width = 8;
format_command(&mut Vec::new(), &config, &[&path], true, None).unwrap();
let output = fs::read(&path).unwrap();
assert_all_line_endings_are_crlf(&output);
}
#[test]
fn format_command_does_not_assume_stdin_is_markdown() {
let mut input = "漢A\n".as_bytes();
let mut output = Vec::new();
format_command_with_reader(
&mut output,
&config(),
&[] as &[PathBuf],
false,
None,
&mut input,
)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "漢A\n");
}
#[test]
fn format_command_language_override_controls_spacing_for_named_files() {
let directory = tempdir().unwrap();
let json = directory.path().join("document.json");
let markdown = directory.path().join("document.md");
fs::write(&json, "漢A\n").unwrap();
fs::write(&markdown, "漢A\n").unwrap();
let mut output = Vec::new();
format_command(
&mut output,
&config(),
&[&json, &markdown],
false,
Some(Language::Markdown),
)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "漢 A\n漢 A\n");
let mut output = Vec::new();
format_command(
&mut output,
&config(),
&[&json, &markdown],
false,
Some(Language::Json),
)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "漢A\n漢A\n");
}
#[test]
fn format_command_language_json_suppresses_spacing_but_still_wraps() {
let mut config = config();
config.max_width = 8;
let source = "{\"漢A\":[\"one\",\"two\",\"three\"]}\n";
let mut input = source.as_bytes();
let mut output = Vec::new();
format_command_with_reader(
&mut output,
&config,
&[] as &[PathBuf],
false,
Some(Language::Json),
&mut input,
)
.unwrap();
let output = String::from_utf8(output).unwrap();
assert_ne!(output, source, "valid JSON should wrap at token seams");
assert!(
output.contains("漢A"),
"JSON selection must suppress Markdown spacing"
);
assert!(!output.contains("漢 A"));
}
#[test]
fn format_command_language_markdown_enables_spacing_for_stdin() {
let mut input = "漢A\n".as_bytes();
let mut output = Vec::new();
format_command_with_reader(
&mut output,
&config(),
&[] as &[PathBuf],
false,
Some(Language::Markdown),
&mut input,
)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "漢 A\n");
}
}