Skip to content

fix(util): read a path's folder and extension from its name - #835

Open
ANIRUDDHA ADAK (aniruddhaadak80) wants to merge 1 commit into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/path-bare-directory
Open

ANIRUDDHA ADAK (aniruddhaadak80) wants to merge 1 commit into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/path-bare-directory

Conversation

@aniruddhaadak80

Copy link
Copy Markdown
Contributor

What

Two helpers in tooling/util/src/path.ts work on the whole path where they should work on the filename:

export function getDirectory(path: string | undefined) {
  const trimmed = path.replace(/[\/\\]+$/, "")
  const parts = trimmed.split(/[\/\\]/)
  return parts.slice(0, parts.length - 1).join("/") + "/"
}

export function getFileExtension(path: string | undefined) {
  const parts = path.split(".")
  return parts[parts.length - 1]
}

Why it matters

A bare filename is reported as living in the filesystem root. getDirectory("foo") splits to ["foo"], the slice is [], and [].join("/") + "/" is "/":

getDirectory("foo")   -> "/"      (a relative name with no folder)
getDirectory("a/b")   -> "a/"

A file with no extension gets its own name as the extension, and a dotfile gets its name minus the dot, and a dot in a directory leaks through:

getFileExtension("README")      -> "README"     (should be "")
getFileExtension(".gitignore")  -> "gitignore"  (should be "")
getFileExtension("a.b/c")       -> "b/c"

This isn't a matter of taste — getFilenameTruncated, four lines below in the same file, already implements the correct rule for the same question (const ext = lastDot <= 0 ? "" : ...). The module contradicted itself.

It's UI-visible: session-turn.tsx:1258 renders `‪${getDirectory(diff.file)}‬` directly, so a root-level filename displays / as its directory, and any caller branching on extension misroutes extensionless and dotfile paths.

Verification

Neither helper had a test. New test/util/path-helpers.test.ts, three cases failing before:

(fail) getDirectory > a bare filename has no directory
(fail) getFileExtension > an extensionless or dotfile name has no extension
(fail) getFileExtension > reads the extension from the filename, not from a directory
 3 pass  3 fail

After:

(pass) getDirectory > a bare filename has no directory
(pass) getDirectory > keeps the directory of a nested path
(pass) getDirectory > the root and an empty input stay as they are
(pass) getFileExtension > an extensionless or dotfile name has no extension
(pass) getFileExtension > reads the extension from the filename, not from a directory
(pass) getFileExtension > an empty or missing path has no extension
 6 pass
 0 fail

The getDirectory("/") case is pinned explicitly: trimming a lone / leaves "", and that is still the root, so the new guard returns / rather than "". The whole test/util/ suite is 59 pass / 0 fail.

The change

 export function getDirectory(path: string | undefined) {
   if (!path) return ""
   const trimmed = path.replace(/[\/\\]+$/, "")
+  // A lone "/" trims away entirely, but it is still the root.
+  if (trimmed === "") return "/"
   const parts = trimmed.split(/[\/\\]/)
+  // A bare filename has no parent, and an empty slice joined to "/" would
+  // report the filesystem root instead.
+  if (parts.length <= 1) return ""
   return parts.slice(0, parts.length - 1).join("/") + "/"
 }
 
 export function getFileExtension(path: string | undefined) {
   if (!path) return ""
-  const parts = path.split(".")
-  return parts[parts.length - 1]
+  // Only the filename carries an extension: a dot in a directory name, or in
+  // an earlier path segment, is not one.
+  const base = getFilename(path)
+  const dot = base.lastIndexOf(".")
+  // A leading dot marks a dotfile, not an extension, and no dot means none.
+  if (dot <= 0) return ""
+  return base.slice(dot + 1)
 }

getFileExtension now reuses getFilename from the same module, which already strips both separators.

Touched files are Prettier-clean (verified on LF-normalized copies; this Windows checkout's core.autocrlf=true makes Prettier flag every file repo-wide).

Fixes #834

@vercel

vercel Bot commented Sep 29, 2026

Copy link
Copy Markdown

ANIRUDDHA ADAK (@aniruddhaadak80) is attempting to deploy a commit to the InkVell Team on Vercel.

A member of the Team first needs to authorize it.

@aniruddhaadak80
ANIRUDDHA ADAK (aniruddhaadak80) force-pushed the fix/path-bare-directory branch 2 times, most recently from c29a906 to 898cf98 Compare September 30, 2026 09:49
getDirectory joined an empty slice to a slash, so a bare filename reported the filesystem root. getFileExtension split the whole path on a dot, so an extensionless name returned itself and a dotfile returned its name minus the dot. Both now work off the filename, and a leading dot is not an extension.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getDirectory reports a bare filename as being in the filesystem root, and getFileExtension returns the filename as the extension

1 participant