diff --git a/crates/ts_goport/src/execute/build/orchestrator.rs b/crates/ts_goport/src/execute/build/orchestrator.rs index 251209caf..3913bd98e 100644 --- a/crates/ts_goport/src/execute/build/orchestrator.rs +++ b/crates/ts_goport/src/execute/build/orchestrator.rs @@ -214,8 +214,16 @@ pub struct Orchestrator { impl Orchestrator { // Go: build/orchestrator.go:94 (*Orchestrator).relativeFileName + // PORT: Go relativePath (:98) is inlined; the name is a file name. pub fn relative_file_name(&self, file_name: &str) -> String { - convert_to_relative_path(file_name, &self.compare_paths_options) + if let Some(relative) = relative_path_from_directory( + &self.compare_paths_options.current_directory, + file_name, + self.compare_paths_options.use_case_sensitive_file_names, + ) { + return relative; + } + file_name.to_string() } // Go: build/orchestrator.go:97 (*Orchestrator).toPath (at 673a5f17d713; removed by diff --git a/crates/ts_goport/src/execute/tsc/diagnostics.rs b/crates/ts_goport/src/execute/tsc/diagnostics.rs index 16c743831..391908f86 100644 --- a/crates/ts_goport/src/execute/tsc/diagnostics.rs +++ b/crates/ts_goport/src/execute/tsc/diagnostics.rs @@ -21,9 +21,23 @@ use super::compile::{System, Writer, write_str}; use super::compile::CommandLineTesting; use crate::diagnostics::Category; use crate::diagnostics_loc::message_localize; -use crate::frontend::tspath::{ComparePathsOptions, convert_to_relative_path, path_is_absolute}; +use crate::frontend::tspath::{ComparePathsOptions, relative_path_from_directory}; use crate::locale::Locale; +// Go: diagnosticwriter/diagnosticwriter.go:27 formatFileNameRelativeTo +// PORT: the directory and the case sensitivity are the fields of the +// `compare_paths_options` of `FormattingOptions`. +pub fn format_file_name_relative_to(file_name: &str, options: &ComparePathsOptions) -> String { + if let Some(relative_path) = relative_path_from_directory( + &options.current_directory, + file_name, + options.use_case_sensitive_file_names, + ) { + return relative_path; + } + file_name.to_string() +} + // Go: diagnosticwriter/diagnosticwriter.go:203 FormattingOptions #[derive(Clone, Debug, Default)] pub struct FormattingOptions { @@ -1051,7 +1065,7 @@ pub fn write_location( let (first_line, first_char) = get_ecma_line_and_utf16_character_of_file_position(&file, pos); let relative_file_name = match format_opts { Some(format_opts) => { - convert_to_relative_path(file.file_name(), &format_opts.compare_paths_options) + format_file_name_relative_to(file.file_name(), &format_opts.compare_paths_options) } None => file.file_name().to_string(), }; @@ -1251,12 +1265,8 @@ fn pretty_path_for_file_error( return String::new(); } let line = get_ecma_line_of_file_position(file, AstDiagnostic(file_errors[0]).pos()); - let mut file_name = file.file_name().to_string(); - if path_is_absolute(&file_name) - && path_is_absolute(&format_opts.compare_paths_options.current_directory) - { - file_name = convert_to_relative_path(file.file_name(), &format_opts.compare_paths_options); - } + let file_name = + format_file_name_relative_to(file.file_name(), &format_opts.compare_paths_options); format!( "{}{}:{}{}", file_name, @@ -1278,7 +1288,7 @@ pub fn write_format_diagnostic( get_ecma_line_and_utf16_character_of_file_position(&file, wrapped.pos()); let file_name = file.file_name(); let relative_file_name = - convert_to_relative_path(file_name, &format_opts.compare_paths_options); + format_file_name_relative_to(file_name, &format_opts.compare_paths_options); write_str( output, &format!("{}({},{}): ", relative_file_name, line + 1, character + 1), diff --git a/crates/ts_goport/src/frontend/compiler/file_include.rs b/crates/ts_goport/src/frontend/compiler/file_include.rs index d97c33876..756dead08 100644 --- a/crates/ts_goport/src/frontend/compiler/file_include.rs +++ b/crates/ts_goport/src/frontend/compiler/file_include.rs @@ -5,32 +5,23 @@ use crate::frontend::prelude::*; /// Go `caseSensitivity.RelativePathFromDirectory(directory, fileName)` and /// its fallback to the absolute name (ts#64159: fileInclude.go:155, -/// program.go:2145). Used for the include-reason text and `explain_files`. -// PORT: the names are normalized, so not reducing "." and ".." (rule R3) -// changes nothing here. Another root, or an empty `directory` (Go tests pass -// ""), gives the absolute name (rule R4). +/// program.go:2159). Used for the include-reason text and `explain_files`. +// PORT: another root, an empty `directory` (Go tests pass "") or a relative +// name that looks rooted (`c:/x` under a directory named `c:`) gives the +// absolute name (rule R4). pub(crate) fn relative_file_name_from_directory( directory: &str, file_name: &str, program: &NewProgram, ) -> String { - let directory_root = &directory[..get_root_length(directory)]; - let file_root = &file_name[..get_root_length(file_name)]; - if directory_root.is_empty() - || !directory_root - .trim_end_matches('/') - .eq_ignore_ascii_case(file_root.trim_end_matches('/')) - { - return file_name.to_string(); - } - get_relative_path_from_directory( + if let Some(relative_path) = relative_path_from_directory( directory, file_name, - &ComparePathsOptions { - use_case_sensitive_file_names: program.use_case_sensitive_file_names(), - current_directory: directory.to_string(), - }, - ) + program.use_case_sensitive_file_names(), + ) { + return relative_path; + } + file_name.to_string() } // Go: fileInclude.go:14 fileIncludeKind diff --git a/crates/ts_goport/src/program.rs b/crates/ts_goport/src/program.rs index fdf778c56..0de419862 100644 --- a/crates/ts_goport/src/program.rs +++ b/crates/ts_goport/src/program.rs @@ -6518,7 +6518,10 @@ pub fn format_diagnostic(diagnostic: &Diagnostic) -> String { }; output.push_str(&format!( "{}({},{}): ", - tspath::convert_to_relative_path(file_name, &compare_options), + crate::execute::tsc::diagnostics::format_file_name_relative_to( + file_name, + &compare_options + ), line + 1, character + 1 )); diff --git a/crates/ts_goport/tests/go_baselines/tsctests/drive_like_segment_path.rs b/crates/ts_goport/tests/go_baselines/tsctests/drive_like_segment_path.rs new file mode 100644 index 000000000..7087ee793 --- /dev/null +++ b/crates/ts_goport/tests/go_baselines/tsctests/drive_like_segment_path.rs @@ -0,0 +1,225 @@ +//! Port-only test of the names of files under a directory named like a +//! drive (`c:`). +//! +//! PORT: no Go counterpart. Go (ts#64159) writes the name of a file relative +//! to the current directory with RelativePathFromDirectory +//! (tspath/relative_path.go:82), which gives the absolute name when the +//! relative name looks rooted: `/c:/a.ts` is `c:/a.ts` relative, which +//! has a DOS root. The port's diagnostic writer (diagnosticwriter.go:27 +//! formatFileNameRelativeTo), `--explainFiles` (program.go:2159, +//! fileInclude.go:155) and `tsc -b` (build/orchestrator.go:94) printed +//! `c:/a.ts`. A file reached through module resolution gets the real path +//! (`/c:/...`, osvfs os.go:142), which is how the fuzzer found it. Only +//! a one-letter name followed by `:` is a DOS root (`x:y`, `ab:c` are not). +//! +//! The test runs `tsgo` in a scratch dir, as the names come from the OS +//! realpath. Every expected output is the output of `tsgo-oracle-3e9236361fa9` +//! on the same files, with its scratch dir replaced. + +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +/// A scratch dir with the files of one case. It is deleted on drop. +struct Project(PathBuf); + +impl Project { + fn new(name: &str, files: &[(&str, &str)]) -> Project { + let dir = std::env::temp_dir().join(format!( + "tsgo_drive_like_segment_{}_{name}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + // The names have the real path of the dir, as Go's realpath. + let dir = std::fs::canonicalize(&dir).unwrap(); + for (file, text) in files { + let path = dir.join(file); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, text).unwrap(); + } + Project(dir) + } + + fn dir(&self) -> String { + self.0.to_str().unwrap().to_string() + } + + /// Runs `tsgo` with `args` and gives the exit code and the output. + fn tsc(&self, args: &[&str]) -> (Option, String) { + let output = Command::new(env!("CARGO_BIN_EXE_tsgo")) + .args(args) + .current_dir(&self.0) + .stdin(Stdio::null()) + .output() + .expect("run tsgo"); + let mut text = String::from_utf8_lossy(&output.stdout).into_owned(); + text.push_str(&String::from_utf8_lossy(&output.stderr)); + (output.status.code(), text) + } +} + +impl Drop for Project { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// `text` without its ANSI escapes. +fn strip_escapes(text: &str) -> String { + let mut out = String::new(); + let mut chars = text.chars(); + while let Some(c) = chars.next() { + if c == '\x1b' { + for c in chars.by_ref() { + if c == 'm' { + break; + } + } + } else { + out.push(c); + } + } + out +} + +/// The fuzzer's repro: a module resolved to `/c:/node_modules/...`. +#[test] +fn resolved_module_under_a_drive_like_dir_has_its_absolute_name() { + let project = Project::new( + "resolved", + &[ + ("c:/root/a.ts", "import \"file4\";\n"), + ( + "c:/node_modules/file4/index.d.ts", + "let x: number = \"s\";\n", + ), + ( + "tsconfig.json", + r#"{"compilerOptions":{"noEmit":true},"files":["c:/root/a.ts"]}"#, + ), + ], + ); + let dir = project.dir(); + let file4 = format!("{dir}/c:/node_modules/file4/index.d.ts"); + let diagnostics = format!( + "{file4}(1,1): error TS1046: Top-level declarations in .d.ts files must start with either a 'declare' or 'export' modifier. +{file4}(1,5): error TS2322: Type 'string' is not assignable to type 'number'. +{file4}(1,17): error TS1039: Initializers are not allowed in ambient contexts. +" + ); + + let (code, output) = project.tsc(&["-p", "tsconfig.json", "--pretty", "false"]); + assert_eq!(output, diagnostics); + assert_eq!(code, Some(2)); + + let (code, output) = + project.tsc(&["-p", "tsconfig.json", "--pretty", "false", "--explainFiles"]); + assert!(output.starts_with(&diagnostics), "{output}"); + let explained = format!( + "{file4} + Imported via \"file4\" from file 'c:/root/a.ts' +c:/root/a.ts + Part of 'files' list in tsconfig.json +" + ); + assert!(output.ends_with(&explained), "{output}"); + assert_eq!(code, Some(2)); +} + +/// A root file named by its absolute path, and names with a `:` that are +/// not a DOS root, in the pretty output and its summary. +#[test] +fn pretty_names_under_a_drive_like_dir_are_absolute() { + let project = Project::new( + "pretty", + &[ + ("c:/root/a.ts", "import \"./b\";\nlet y: string = 1;\n"), + ("c:/root/b.ts", "export {}; let z: string = 2;\n"), + ("x:y/c.ts", "let w: string = 3;\n"), + ("ab:c/e.ts", "let v: string = 4;\n"), + ], + ); + let dir = project.dir(); + std::fs::write( + project.0.join("tsconfig.json"), + format!( + r#"{{"compilerOptions":{{"noEmit":true}},"files":["{dir}/c:/root/a.ts","x:y/c.ts","ab:c/e.ts"]}}"# + ), + ) + .unwrap(); + + let (code, output) = project.tsc(&["-p", "tsconfig.json", "--pretty", "true"]); + assert_eq!( + strip_escapes(&output), + format!( + "ab:c/e.ts:1:5 - error TS2322: Type 'number' is not assignable to type 'string'. + +1 let v: string = 4; + ~ + +{dir}/c:/root/a.ts:2:5 - error TS2322: Type 'number' is not assignable to type 'string'. + +2 let y: string = 1; + ~ + +{dir}/c:/root/b.ts:1:16 - error TS2322: Type 'number' is not assignable to type 'string'. + +1 export {{}}; let z: string = 2; + ~ + +x:y/c.ts:1:5 - error TS2322: Type 'number' is not assignable to type 'string'. + +1 let w: string = 3; + ~ + + +Found 4 errors in 4 files. + +Errors Files + 1 ab:c/e.ts:1 + 1 {dir}/c:/root/a.ts:2 + 1 {dir}/c:/root/b.ts:1 + 1 x:y/c.ts:1 + +" + ) + ); + assert_eq!(code, Some(2)); +} + +/// `tsc -b --verbose` of a project under a drive-like dir. +#[test] +fn build_names_under_a_drive_like_dir_are_absolute() { + let project = Project::new( + "build", + &[ + ("c:/p/a.ts", "export const a = 1;\nlet x: number = \"s\";\n"), + ( + "c:/p/tsconfig.json", + r#"{"compilerOptions":{"outDir":"out"},"files":["a.ts"]}"#, + ), + ], + ); + let dir = project.dir(); + let config = format!("{dir}/c:/p"); + let (code, output) = project.tsc(&["-b", &config, "--verbose", "--pretty", "false"]); + // Without the time of each status line. + let output = output + .lines() + .map(|line| match line.split_once(" - ") { + Some((time, rest)) if time.ends_with("AM") || time.ends_with("PM") => rest, + _ => line, + }) + .collect::>() + .join("\n"); + assert_eq!( + output, + format!( + "Projects in this build: \n * {config}/tsconfig.json\n\n\ + Project '{config}/tsconfig.json' is out of date because output file '{config}/out/tsconfig.tsbuildinfo' does not exist\n\n\ + Building project '{config}/tsconfig.json'...\n\n\ + {config}/a.ts(2,5): error TS2322: Type 'string' is not assignable to type 'number'." + ) + ); + assert_eq!(code, Some(2)); +} diff --git a/crates/ts_goport/tests/go_baselines/tsctests/mod.rs b/crates/ts_goport/tests/go_baselines/tsctests/mod.rs index 2f3d421fb..b938b5fab 100644 --- a/crates/ts_goport/tests/go_baselines/tsctests/mod.rs +++ b/crates/ts_goport/tests/go_baselines/tsctests/mod.rs @@ -3,6 +3,7 @@ mod build_info_corrupt; mod build_prefetch_rereads; mod checker_common_source_directory; mod contentmapper_watch; +mod drive_like_segment_path; mod effect_build_info; mod explain_files_cache; mod extended_counts;