What's wrong
AbsoluteFilePath.RemoveExtension() (Semantics.Paths/Implementations/AbsoluteFilePath.cs around lines 76–79) and the same method on RelativeFilePath do this:
string pathWithoutExtension = Path.ChangeExtension(WeakString, null) ?? "";
return Create<AbsoluteFilePath>(pathWithoutExtension);
.NET treats a leading-dot file name as consisting entirely of an extension. So Path.ChangeExtension("/home/user/.bashrc", null) returns "/home/user/". SemanticPath.MakeCanonical then strips the trailing separator, and the result is the parent directory, typed as a file path.
Repro (net10.0, Linux)
| Call |
Result |
AbsoluteFilePath.Create("/home/user/.bashrc").RemoveExtension() |
"/home/user", the same as its own AbsoluteDirectoryPath |
AbsoluteFilePath.Create("/.profile").RemoveExtension() |
"/", the filesystem root as a file |
RelativeFilePath.Create("src/.editorconfig").RemoveExtension() |
"src" |
RelativeFilePath.Create(".gitignore").RemoveExtension() |
"" |
ChangeExtension(".bak") on /home/user/.bashrc |
"/home/user/.bak", which renames the file instead of adding an extension |
Why it matters
Code such as File.Move(path, path.RemoveExtension()) or a cleanup that deletes path.RemoveExtension() silently targets a directory. The strong typing is supposed to rule out exactly this kind of mistake.
Suggested fix
Operate on the file name only. If Path.GetFileName has no . after index 0, RemoveExtension() returns the path unchanged and ChangeExtension appends the new extension. Apply the same dotfile rule to the FileExtension/FullFileExtension getters that #287 is deciding, so all extension members agree.
Acceptance: tests cover /home/user/.bashrc, /.profile, .gitignore and src/.editorconfig for both RemoveExtension and ChangeExtension, on absolute and relative file paths. RemoveExtension returns a path whose file name is unchanged, and ChangeExtension(".bak") yields .bashrc.bak.
What's wrong
AbsoluteFilePath.RemoveExtension()(Semantics.Paths/Implementations/AbsoluteFilePath.csaround lines 76–79) and the same method onRelativeFilePathdo this:.NET treats a leading-dot file name as consisting entirely of an extension. So
Path.ChangeExtension("/home/user/.bashrc", null)returns"/home/user/".SemanticPath.MakeCanonicalthen strips the trailing separator, and the result is the parent directory, typed as a file path.Repro (net10.0, Linux)
AbsoluteFilePath.Create("/home/user/.bashrc").RemoveExtension()"/home/user", the same as its ownAbsoluteDirectoryPathAbsoluteFilePath.Create("/.profile").RemoveExtension()"/", the filesystem root as a fileRelativeFilePath.Create("src/.editorconfig").RemoveExtension()"src"RelativeFilePath.Create(".gitignore").RemoveExtension()""ChangeExtension(".bak")on/home/user/.bashrc"/home/user/.bak", which renames the file instead of adding an extensionWhy it matters
Code such as
File.Move(path, path.RemoveExtension())or a cleanup that deletespath.RemoveExtension()silently targets a directory. The strong typing is supposed to rule out exactly this kind of mistake.Suggested fix
Operate on the file name only. If
Path.GetFileNamehas no.after index 0,RemoveExtension()returns the path unchanged andChangeExtensionappends the new extension. Apply the same dotfile rule to theFileExtension/FullFileExtensiongetters that #287 is deciding, so all extension members agree.Acceptance: tests cover
/home/user/.bashrc,/.profile,.gitignoreandsrc/.editorconfigfor bothRemoveExtensionandChangeExtension, on absolute and relative file paths.RemoveExtensionreturns a path whose file name is unchanged, andChangeExtension(".bak")yields.bashrc.bak.