diff --git a/src/bin/rustfmt.rs b/src/bin/rustfmt.rs index f4b9c081821..0e48ab7c23a 100644 --- a/src/bin/rustfmt.rs +++ b/src/bin/rustfmt.rs @@ -8,6 +8,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +#![feature(rustc_private)] #![cfg(not(test))] extern crate env_logger; @@ -22,7 +23,7 @@ use std::str::FromStr; use getopts::{Matches, Options}; -use rustfmt::{run, Input, Summary}; +use rustfmt::{run, FileName, Input, Summary}; use rustfmt::file_lines::FileLines; use rustfmt::config::{get_toml_path, Color, Config, WriteMode}; @@ -243,8 +244,9 @@ fn execute(opts: &Options) -> FmtResult { if let Some(ref file_lines) = matches.opt_str("file-lines") { config.set().file_lines(file_lines.parse()?); for f in config.file_lines().files() { - if f != "stdin" { - eprintln!("Warning: Extra file listed in file_lines option '{}'", f); + match *f { + FileName::Custom(ref f) if f == "stdin" => {} + _ => eprintln!("Warning: Extra file listed in file_lines option '{}'", f), } } } @@ -264,8 +266,12 @@ fn execute(opts: &Options) -> FmtResult { let options = CliOptions::from_matches(&matches)?; for f in options.file_lines.files() { - if !files.contains(&PathBuf::from(f)) { - eprintln!("Warning: Extra file listed in file_lines option '{}'", f); + match *f { + FileName::Real(ref f) if files.contains(f) => {} + FileName::Real(_) => { + eprintln!("Warning: Extra file listed in file_lines option '{}'", f) + } + _ => eprintln!("Warning: Not a file '{}'", f), } } diff --git a/src/checkstyle.rs b/src/checkstyle.rs index cbb6573b322..7f6e650ad22 100644 --- a/src/checkstyle.rs +++ b/src/checkstyle.rs @@ -9,6 +9,7 @@ // except according to those terms. use std::io::{self, Write}; +use std::path::Path; use config::WriteMode; use rustfmt_diff::{DiffLine, Mismatch}; @@ -41,13 +42,13 @@ where pub fn output_checkstyle_file( mut writer: T, - filename: &str, + filename: &Path, diff: Vec, ) -> Result<(), io::Error> where T: Write, { - write!(writer, "", filename)?; + write!(writer, "", filename.display())?; for mismatch in diff { for line in mismatch.lines { // Do nothing with `DiffLine::Context` and `DiffLine::Resulting`. diff --git a/src/codemap.rs b/src/codemap.rs index efbe50bcf64..e0401b0b197 100644 --- a/src/codemap.rs +++ b/src/codemap.rs @@ -13,7 +13,7 @@ use std::rc::Rc; -use syntax::codemap::{BytePos, CodeMap, FileMap, Span}; +use syntax::codemap::{BytePos, CodeMap, FileMap, FileName, Span}; use comment::FindUncommented; @@ -25,8 +25,8 @@ pub struct LineRange { } impl LineRange { - pub fn file_name(&self) -> &str { - self.file.as_ref().name.as_str() + pub fn file_name(&self) -> &FileName { + &self.file.name } } diff --git a/src/file_lines.rs b/src/file_lines.rs index a131038eb4e..82844a4b222 100644 --- a/src/file_lines.rs +++ b/src/file_lines.rs @@ -10,12 +10,14 @@ //! This module contains types and functions to support formatting specific line ranges. -use std::{cmp, iter, path, str}; +use std::{cmp, iter, str}; use std::collections::HashMap; +use serde::de::{Deserialize, Deserializer}; use serde_json as json; use codemap::LineRange; +use syntax::codemap::FileName; /// A range that is inclusive of both ends. #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize)] @@ -84,11 +86,11 @@ impl Range { /// non-overlapping ranges sorted by their start point. An inner `None` is interpreted to mean all /// lines in all files. #[derive(Clone, Debug, Default)] -pub struct FileLines(Option>>); +pub struct FileLines(Option>>); /// Normalizes the ranges so that the invariants for `FileLines` hold: ranges are non-overlapping, /// and ordered by their start point. -fn normalize_ranges(ranges: &mut HashMap>) { +fn normalize_ranges(ranges: &mut HashMap>) { for ranges in ranges.values_mut() { ranges.sort(); let mut result = vec![]; @@ -117,7 +119,7 @@ impl FileLines { FileLines(None) } - pub fn from_ranges(mut ranges: HashMap>) -> FileLines { + pub fn from_ranges(mut ranges: HashMap>) -> FileLines { normalize_ranges(&mut ranges); FileLines(Some(ranges)) } @@ -129,7 +131,7 @@ impl FileLines { /// Returns true if `self` includes all lines in all files. Otherwise runs `f` on all ranges in /// the designated file (if any) and returns true if `f` ever does. - fn file_range_matches(&self, file_name: &str, f: F) -> bool + fn file_range_matches(&self, file_name: &FileName, f: F) -> bool where F: FnMut(&Range) -> bool, { @@ -156,35 +158,31 @@ impl FileLines { } /// Returns true if `line` from `file_name` is in `self`. - pub fn contains_line(&self, file_name: &str, line: usize) -> bool { + pub fn contains_line(&self, file_name: &FileName, line: usize) -> bool { self.file_range_matches(file_name, |r| r.lo <= line && r.hi >= line) } /// Returns true if any of the lines between `lo` and `hi` from `file_name` are in `self`. - pub fn intersects_range(&self, file_name: &str, lo: usize, hi: usize) -> bool { + pub fn intersects_range(&self, file_name: &FileName, lo: usize, hi: usize) -> bool { self.file_range_matches(file_name, |r| r.intersects(Range::new(lo, hi))) } } /// `FileLines` files iterator. -pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, String, Vec>>); +pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, FileName, Vec>>); impl<'a> iter::Iterator for Files<'a> { - type Item = &'a String; + type Item = &'a FileName; - fn next(&mut self) -> Option<&'a String> { + fn next(&mut self) -> Option<&'a FileName> { self.0.as_mut().and_then(Iterator::next) } } -fn canonicalize_path_string(s: &str) -> Option { - if s == "stdin" { - return Some(s.to_string()); - } - - match path::PathBuf::from(s).canonicalize() { - Ok(canonicalized) => canonicalized.to_str().map(|s| s.to_string()), - _ => None, +fn canonicalize_path_string(file: &FileName) -> Option { + match *file { + FileName::Real(ref path) => path.canonicalize().ok().map(FileName::Real), + _ => Some(file.clone()), } } @@ -206,12 +204,21 @@ impl str::FromStr for FileLines { // For JSON decoding. #[derive(Clone, Debug, Deserialize)] struct JsonSpan { - file: String, + #[serde(deserialize_with = "deserialize_filename")] file: FileName, range: (usize, usize), } +fn deserialize_filename<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + if s == "stdin" { + Ok(FileName::Custom(s)) + } else { + Ok(FileName::Real(s.into())) + } +} + impl JsonSpan { - fn into_tuple(self) -> Result<(String, Range), String> { + fn into_tuple(self) -> Result<(FileName, Range), String> { let (lo, hi) = self.range; let canonical = canonicalize_path_string(&self.file) .ok_or_else(|| format!("Can't canonicalize {}", &self.file))?; diff --git a/src/filemap.rs b/src/filemap.rs index c7fa5e2f1ef..5678dd59c55 100644 --- a/src/filemap.rs +++ b/src/filemap.rs @@ -12,15 +12,17 @@ use std::fs::{self, File}; use std::io::{self, BufWriter, Read, Write}; +use std::path::Path; use checkstyle::{output_checkstyle_file, output_footer, output_header}; use config::{Config, NewlineStyle, WriteMode}; use rustfmt_diff::{make_diff, print_diff, Mismatch}; +use syntax::codemap::FileName; // A map of the files of a crate, with their new content pub type FileMap = Vec; -pub type FileRecord = (String, String); +pub type FileRecord = (FileName, String); // Append a newline to the end of each file. pub fn append_newline(s: &mut String) { @@ -80,7 +82,7 @@ where pub fn write_file( text: &str, - filename: &str, + filename: &FileName, out: &mut T, config: &Config, ) -> Result @@ -89,7 +91,7 @@ where { fn source_and_formatted_text( text: &str, - filename: &str, + filename: &Path, config: &Config, ) -> Result<(String, String), io::Error> { let mut f = File::open(filename)?; @@ -102,7 +104,7 @@ where } fn create_diff( - filename: &str, + filename: &Path, text: &str, config: &Config, ) -> Result, io::Error> { @@ -110,15 +112,21 @@ where Ok(make_diff(&ori, &fmt, 3)) } + let filename_to_path = || match *filename { + FileName::Real(ref path) => path, + _ => panic!("cannot format `{}` with WriteMode::Replace", filename), + }; + match config.write_mode() { WriteMode::Replace => { - if let Ok((ori, fmt)) = source_and_formatted_text(text, filename, config) { + let filename = filename_to_path(); + if let Ok((ori, fmt)) = source_and_formatted_text(text, &filename, config) { if fmt != ori { // Do a little dance to make writing safer - write to a temp file // rename the original to a .bk, then rename the temp file to the // original. - let tmp_name = filename.to_owned() + ".tmp"; - let bk_name = filename.to_owned() + ".bk"; + let tmp_name = filename.with_extension("tmp"); + let bk_name = filename.with_extension("bk"); { // Write text to temp file let tmp_file = File::create(&tmp_name)?; @@ -132,7 +140,8 @@ where } WriteMode::Overwrite => { // Write text directly over original file if there is a diff. - let (source, formatted) = source_and_formatted_text(text, filename, config)?; + let filename = filename_to_path(); + let (source, formatted) = source_and_formatted_text(text, &filename, config)?; if source != formatted { let file = File::create(filename)?; write_system_newlines(file, text, config)?; @@ -146,19 +155,21 @@ where write_system_newlines(out, text, config)?; } WriteMode::Diff => { - if let Ok((ori, fmt)) = source_and_formatted_text(text, filename, config) { + let filename = filename_to_path(); + if let Ok((ori, fmt)) = source_and_formatted_text(text, &filename, config) { let mismatch = make_diff(&ori, &fmt, 3); let has_diff = !mismatch.is_empty(); print_diff( mismatch, - |line_num| format!("Diff in {} at line {}:", filename, line_num), + |line_num| format!("Diff in {} at line {}:", filename.display(), line_num), config.color(), ); return Ok(has_diff); } } WriteMode::Checkstyle => { - let diff = create_diff(filename, text, config)?; + let filename = filename_to_path(); + let diff = create_diff(&filename, text, config)?; output_checkstyle_file(out, filename, diff)?; } } diff --git a/src/lib.rs b/src/lib.rs index b00caf40cd5..14914359bdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,13 +29,14 @@ use std::collections::HashMap; use std::fmt; use std::io::{self, stdout, Write}; use std::iter::repeat; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::rc::Rc; use errors::{DiagnosticBuilder, Handler}; use errors::emitter::{ColorConfig, EmitterWriter}; use syntax::ast; use syntax::codemap::{CodeMap, FilePathMapping}; +pub use syntax::codemap::FileName; use syntax::parse::{self, ParseSess}; use checkstyle::{output_footer, output_header}; @@ -146,7 +147,7 @@ impl FormattingError { pub struct FormatReport { // Maps stringified file paths to their associated formatting errors. - file_error_map: HashMap>, + file_error_map: HashMap>, } impl FormatReport { @@ -295,12 +296,12 @@ impl fmt::Display for FormatReport { fn format_ast( krate: &ast::Crate, parse_session: &mut ParseSess, - main_file: &Path, + main_file: &FileName, config: &Config, mut after_file: F, ) -> Result<(FileMap, bool), io::Error> where - F: FnMut(&str, &mut String, &[(usize, usize)]) -> Result, + F: FnMut(&FileName, &mut String, &[(usize, usize)]) -> Result, { let mut result = FileMap::new(); // diff mode: check if any files are differing @@ -310,12 +311,11 @@ where // nothing to distinguish the nested module contents. let skip_children = config.skip_children() || config.write_mode() == config::WriteMode::Plain; for (path, module) in modules::list_files(krate, parse_session.codemap())? { - if skip_children && path.as_path() != main_file { + if skip_children && path != *main_file { continue; } - let path_str = path.to_str().unwrap(); if config.verbose() { - println!("Formatting {}", path_str); + println!("Formatting {}", path); } let filemap = parse_session .codemap() @@ -325,7 +325,7 @@ where let snippet_provider = SnippetProvider::new(filemap.start_pos, big_snippet); let mut visitor = FmtVisitor::from_codemap(parse_session, config, &snippet_provider); // Format inner attributes if available. - if !krate.attrs.is_empty() && path == main_file { + if !krate.attrs.is_empty() && path == *main_file { visitor.skip_empty_lines(filemap.end_pos); if visitor.visit_attrs(&krate.attrs, ast::AttrStyle::Inner) { visitor.push_rewrite(module.inner, None); @@ -343,16 +343,17 @@ where ::utils::count_newlines(&format!("{}", visitor.buffer)) ); - has_diff |= match after_file(path_str, &mut visitor.buffer, &visitor.skipped_range) { + let filename = path.clone(); + has_diff |= match after_file(&filename, &mut visitor.buffer, &visitor.skipped_range) { Ok(result) => result, Err(e) => { // Create a new error with path_str to help users see which files failed - let err_msg = path_str.to_string() + &": ".to_string() + &e.to_string(); + let err_msg = format!("{}: {}", path, e); return Err(io::Error::new(e.kind(), err_msg)); } }; - result.push((path_str.to_owned(), visitor.buffer)); + result.push((filename, visitor.buffer)); } Ok((result, has_diff)) @@ -389,7 +390,7 @@ fn should_report_error( // FIXME(#20) other stuff for parity with make tidy fn format_lines( text: &mut String, - name: &str, + name: &FileName, skipped_range: &[(usize, usize)], config: &Config, report: &mut FormatReport, @@ -491,7 +492,7 @@ fn format_lines( } } - report.file_error_map.insert(name.to_owned(), errors); + report.file_error_map.insert(name.clone(), errors); } fn parse_input( @@ -505,8 +506,11 @@ fn parse_input( parser.parse_crate_mod() } Input::Text(text) => { - let mut parser = - parse::new_parser_from_source_str(parse_session, "stdin".to_owned(), text); + let mut parser = parse::new_parser_from_source_str( + parse_session, + FileName::Custom("stdin".to_owned()), + text, + ); parser.cfg_mods = false; parser.parse_crate_mod() } @@ -547,8 +551,8 @@ pub fn format_input( let mut parse_session = ParseSess::with_span_handler(tty_handler, codemap.clone()); let main_file = match input { - Input::File(ref file) => file.clone(), - Input::Text(..) => PathBuf::from("stdin"), + Input::File(ref file) => FileName::Real(file.clone()), + Input::Text(..) => FileName::Custom("stdin".to_owned()), }; let krate = match parse_input(input, &parse_session) { diff --git a/src/missed_spans.rs b/src/missed_spans.rs index 4e1f2a37ef4..718ac6bfaab 100644 --- a/src/missed_spans.rs +++ b/src/missed_spans.rs @@ -11,7 +11,7 @@ use std::borrow::Cow; use std::iter::repeat; -use syntax::codemap::{BytePos, Pos, Span}; +use syntax::codemap::{BytePos, FileName, Pos, Span}; use codemap::LineRangeUtils; use comment::{rewrite_comment, CodeCharKind, CommentCodeSlices}; @@ -260,7 +260,7 @@ impl<'a> FmtVisitor<'a> { snippet: &str, subslice: &str, offset: usize, - file_name: &str, + file_name: &FileName, ) { for (mut i, c) in subslice.char_indices() { i += offset; diff --git a/src/modules.rs b/src/modules.rs index 30deafd3f9e..ce63597b394 100644 --- a/src/modules.rs +++ b/src/modules.rs @@ -13,7 +13,7 @@ use std::path::{Path, PathBuf}; use std::io; use syntax::ast; -use syntax::codemap; +use syntax::codemap::{self, FileName}; use syntax::parse::parser; use utils::contains_skip; @@ -23,15 +23,16 @@ use utils::contains_skip; pub fn list_files<'a>( krate: &'a ast::Crate, codemap: &codemap::CodeMap, -) -> Result, io::Error> { +) -> Result, io::Error> { let mut result = BTreeMap::new(); // Enforce file order determinism - let root_filename: PathBuf = codemap.span_to_filename(krate.span).into(); - list_submodules( - &krate.module, - root_filename.parent().unwrap(), - codemap, - &mut result, - )?; + let root_filename = codemap.span_to_filename(krate.span); + { + let parent = match root_filename { + FileName::Real(ref path) => path.parent().unwrap(), + _ => Path::new(""), + }; + list_submodules(&krate.module, parent, codemap, &mut result)?; + } result.insert(root_filename, &krate.module); Ok(result) } @@ -41,7 +42,7 @@ fn list_submodules<'a>( module: &'a ast::Mod, search_dir: &Path, codemap: &codemap::CodeMap, - result: &mut BTreeMap, + result: &mut BTreeMap, ) -> Result<(), io::Error> { debug!("list_submodules: search_dir: {:?}", search_dir); for item in &module.items { @@ -54,7 +55,7 @@ fn list_submodules<'a>( } else { let mod_path = module_file(item.ident, &item.attrs, search_dir, codemap)?; let dir_path = mod_path.parent().unwrap().to_owned(); - result.insert(mod_path, sub_mod); + result.insert(FileName::Real(mod_path), sub_mod); dir_path }; list_submodules(sub_mod, &dir_path, codemap, result)?; diff --git a/tests/system.rs b/tests/system.rs index 3bb41b1cd6b..212e23f4f10 100644 --- a/tests/system.rs +++ b/tests/system.rs @@ -8,6 +8,8 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +#![feature(rustc_private)] + #[macro_use] extern crate log; extern crate regex; @@ -28,10 +30,8 @@ use rustfmt::rustfmt_diff::*; const DIFF_CONTEXT_SIZE: usize = 3; -fn get_path_string(dir_entry: io::Result) -> String { - let path = dir_entry.expect("Couldn't get DirEntry").path(); - - path.to_str().expect("Couldn't stringify path").to_owned() +fn get_path_string(dir_entry: io::Result) -> PathBuf { + dir_entry.expect("Couldn't get DirEntry").path().to_owned() } // Integration tests. The files in the tests/source are formatted and compared @@ -68,12 +68,12 @@ fn coverage_tests() { fn checkstyle_test() { let filename = "tests/writemode/source/fn-single-line.rs"; let expected_filename = "tests/writemode/target/checkstyle.xml"; - assert_output(filename, expected_filename); + assert_output(Path::new(filename), Path::new(expected_filename)); } // Helper function for comparing the results of rustfmt // to a known output file generated by one of the write modes. -fn assert_output(source: &str, expected_filename: &str) { +fn assert_output(source: &Path, expected_filename: &Path) { let config = read_config(source); let (file_map, _report) = format_file(source, &config); @@ -91,7 +91,7 @@ fn assert_output(source: &str, expected_filename: &str) { let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE); if !compare.is_empty() { let mut failures = HashMap::new(); - failures.insert(source.to_string(), compare); + failures.insert(source.to_owned(), compare); print_mismatches(failures); assert!(false, "Text does not match expected output"); } @@ -121,8 +121,8 @@ fn self_tests() { .chain(fs::read_dir("tests").expect("Couldn't read tests dir")) .map(get_path_string); // Hack because there's no `IntoIterator` impl for `[T; N]`. - let files = files.chain(Some("src/lib.rs".to_owned()).into_iter()); - let files = files.chain(Some("build.rs".to_owned()).into_iter()); + let files = files.chain(Some(PathBuf::from("src/lib.rs")).into_iter()); + let files = files.chain(Some(PathBuf::from("build.rs")).into_iter()); let (reports, count, fails) = check_files(files); let mut warnings = 0; @@ -152,9 +152,11 @@ fn stdin_formatting_smoke_test() { format_input::(input, &config, None).unwrap(); assert!(error_summary.has_no_errors()); for &(ref file_name, ref text) in &file_map { - if file_name == "stdin" { - assert_eq!(text.to_string(), "fn main() {}\n"); - return; + if let FileName::Custom(ref file_name) = *file_name { + if file_name == "stdin" { + assert_eq!(text.to_string(), "fn main() {}\n"); + return; + } } } panic!("no stdin"); @@ -197,14 +199,14 @@ fn format_lines_errors_are_reported() { // Returns the number of files checked and the number of failures. fn check_files(files: I) -> (Vec, u32, u32) where - I: Iterator, + I: Iterator, { let mut count = 0; let mut fails = 0; let mut reports = vec![]; - for file_name in files.filter(|f| f.ends_with(".rs")) { - debug!("Testing '{}'...", file_name); + for file_name in files.filter(|f| f.extension().map_or(false, |f| f == "rs")) { + debug!("Testing '{}'...", file_name.display()); match idempotent_check(file_name) { Ok(ref report) if report.has_warnings() => { @@ -224,13 +226,13 @@ where (reports, count, fails) } -fn print_mismatches(result: HashMap>) { +fn print_mismatches(result: HashMap>) { let mut t = term::stdout().unwrap(); for (file_name, diff) in result { print_diff( diff, - |line_num| format!("\nMismatch at {}:{}:", file_name, line_num), + |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num), Color::Auto, ); } @@ -238,20 +240,15 @@ fn print_mismatches(result: HashMap>) { t.reset().unwrap(); } -fn read_config(filename: &str) -> Config { +fn read_config(filename: &Path) -> Config { let sig_comments = read_significant_comments(filename); // Look for a config file... If there is a 'config' property in the significant comments, use // that. Otherwise, if there are no significant comments at all, look for a config file with // the same name as the test file. let mut config = if !sig_comments.is_empty() { - get_config(sig_comments.get("config").map(|x| &(*x)[..])) + get_config(sig_comments.get("config").map(Path::new)) } else { - get_config( - Path::new(filename) - .with_extension("toml") - .file_name() - .and_then(std::ffi::OsStr::to_str), - ) + get_config(filename.with_extension("toml").file_name().map(Path::new)) }; for (key, val) in &sig_comments { @@ -274,7 +271,9 @@ fn format_file>(filepath: P, config: &Config) -> (FileMap, Form (file_map, report) } -pub fn idempotent_check(filename: String) -> Result>> { +pub fn idempotent_check( + filename: PathBuf, +) -> Result>> { let sig_comments = read_significant_comments(&filename); let config = read_config(&filename); let (file_map, format_report) = format_file(filename, &config); @@ -286,7 +285,9 @@ pub fn idempotent_check(filename: String) -> Result Result) -> Config { +fn get_config(config_file: Option<&Path>) -> Config { let config_file_name = match config_file { None => return Default::default(), Some(file_name) => { - let mut full_path = "tests/config/".to_owned(); - full_path.push_str(file_name); - if !Path::new(&full_path).exists() { + let mut full_path = PathBuf::from("tests/config/"); + full_path.push(file_name); + if !full_path.exists() { return Default::default(); }; full_path @@ -321,8 +322,9 @@ fn get_config(config_file: Option<&str>) -> Config { // Reads significant comments of the form: // rustfmt-key: value // into a hash map. -fn read_significant_comments(file_name: &str) -> HashMap { - let file = fs::File::open(file_name).expect(&format!("Couldn't read file {}", file_name)); +fn read_significant_comments(file_name: &Path) -> HashMap { + let file = + fs::File::open(file_name).expect(&format!("Couldn't read file {}", file_name.display())); let reader = BufReader::new(file); let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)"; let regex = regex::Regex::new(pattern).expect("Failed creating pattern 1"); @@ -357,9 +359,9 @@ fn read_significant_comments(file_name: &str) -> HashMap { // Compare output to input. // TODO: needs a better name, more explanation. fn handle_result( - result: HashMap, + result: HashMap, target: Option<&str>, -) -> Result<(), HashMap>> { +) -> Result<(), HashMap>> { let mut failures = HashMap::new(); for (file_name, fmt_text) in result { @@ -391,15 +393,21 @@ fn handle_result( } // Map source file paths to their target paths. -fn get_target(file_name: &str, target: Option<&str>) -> String { - if file_name.contains("source") { - let target_file_name = file_name.replace("source", "target"); +fn get_target(file_name: &Path, target: Option<&str>) -> PathBuf { + if let Some(n) = file_name + .components() + .position(|c| c.as_os_str() == "source") + { + let mut target_file_name = PathBuf::new(); + for (i, c) in file_name.components().enumerate() { + if i == n { + target_file_name.push("target"); + } else { + target_file_name.push(c.as_os_str()); + } + } if let Some(replace_name) = target { - Path::new(&target_file_name) - .with_file_name(replace_name) - .into_os_string() - .into_string() - .unwrap() + target_file_name.with_file_name(replace_name) } else { target_file_name }