diff --git a/knip.json b/knip.json index f52a24bb0..52292d8c1 100644 --- a/knip.json +++ b/knip.json @@ -2,7 +2,6 @@ "$schema": "./node_modules/knip/schema.json", "ignore": ["packages/e2e/**/*"], "ignoreExportsUsedInFile": { "interface": true, "type": true }, - "ignoreUnresolved": ["./vitest.setup.ts"], "treatConfigHintsAsErrors": true, "workspaces": { ".": { diff --git a/packages/cli/src/runCliWatch.ts b/packages/cli/src/runCliWatch.ts index 3d236ebff..32eab27dd 100644 --- a/packages/cli/src/runCliWatch.ts +++ b/packages/cli/src/runCliWatch.ts @@ -1,5 +1,5 @@ import type { LinterHost, LintResults } from "@flint.fyi/core"; -import { normalizePath } from "@flint.fyi/core"; +import { pathKey } from "@flint.fyi/utils"; import debounce from "debounce"; import { debugForFile } from "debug-for-file"; @@ -16,6 +16,7 @@ export async function runCliWatch( values: OptionsValues, ) { const cwd = host.getCurrentDirectory(); + const isCaseSensitiveFS = host.isCaseSensitiveFS(); log("Running single-run CLI once before watching"); @@ -54,7 +55,7 @@ export async function runCliWatch( currentRenderer = startNewTask(true); const rerun = debounce((fileName: string) => { - const normalizedPath = normalizePath(fileName, true); + const normalizedPath = pathKey(fileName, isCaseSensitiveFS); const shouldRerun = shouldRerunForFileChange( normalizedPath, diff --git a/packages/core/src/host/createDiskBackedLinterHost.test.ts b/packages/core/src/host/createDiskBackedLinterHost.test.ts index e2f491b6d..c5eb90afa 100644 --- a/packages/core/src/host/createDiskBackedLinterHost.test.ts +++ b/packages/core/src/host/createDiskBackedLinterHost.test.ts @@ -1,10 +1,10 @@ +import { normalizePath } from "@flint.fyi/utils"; import fs from "node:fs"; import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createDiskBackedLinterHost } from "./createDiskBackedLinterHost.ts"; -import { normalizePath } from "./normalizePath.ts"; const INTEGRATION_DIR_NAME = ".flint-disk-backed-linter-host-integration-tests"; @@ -41,9 +41,7 @@ describe("createDiskBackedLinterHost", () => { it("normalizes cwd", () => { const host = createDiskBackedLinterHost(integrationRoot + "/dir/.."); - expect(host.getCurrentDirectory()).toEqual( - normalizePath(integrationRoot, host.isCaseSensitiveFS()), - ); + expect(host.getCurrentDirectory()).toEqual(normalizePath(integrationRoot)); }); it("stats files and directories", () => { @@ -366,10 +364,7 @@ describe("createDiskBackedLinterHost", () => { fs.writeFileSync(nestedFile, "nested"); fs.writeFileSync(directFile, "direct"); - const normalizedDirect = normalizePath( - directFile, - host.isCaseSensitiveFS(), - ); + const normalizedDirect = normalizePath(directFile); await vi.waitFor(() => { expect(onEvent).toHaveBeenCalledWith(normalizedDirect); @@ -392,10 +387,7 @@ describe("createDiskBackedLinterHost", () => { const nestedFile = path.join(nestedPath, "nested.txt"); fs.writeFileSync(nestedFile, "nested"); - const normalizedNested = normalizePath( - nestedFile, - host.isCaseSensitiveFS(), - ); + const normalizedNested = normalizePath(nestedFile); await vi.waitFor(() => { expect(onEvent).toHaveBeenCalledWith(normalizedNested); @@ -415,10 +407,7 @@ describe("createDiskBackedLinterHost", () => { fs.writeFileSync(path.join(baseDir, ".git", "config"), "content"); fs.writeFileSync(path.join(baseDir, "src.txt"), "content"); - const normalizedFile = normalizePath( - path.join(baseDir, "src.txt"), - host.isCaseSensitiveFS(), - ); + const normalizedFile = normalizePath(path.join(baseDir, "src.txt")); await sleep(50); expect(onEvent).toHaveBeenCalledWith(normalizedFile); }); @@ -441,10 +430,7 @@ describe("createDiskBackedLinterHost", () => { ); fs.writeFileSync(path.join(baseDir, "src.txt"), "content"); - const normalizedFile = normalizePath( - path.join(baseDir, "src.txt"), - host.isCaseSensitiveFS(), - ); + const normalizedFile = normalizePath(path.join(baseDir, "src.txt")); await sleep(50); expect(onEvent).toHaveBeenCalledWith(normalizedFile); }); @@ -461,7 +447,7 @@ describe("createDiskBackedLinterHost", () => { const filePath = path.join(baseDir, ".gitignore"); fs.writeFileSync(filePath, "content"); - const normalizedFile = normalizePath(filePath, host.isCaseSensitiveFS()); + const normalizedFile = normalizePath(filePath); await vi.waitFor(() => { expect(onEvent).toHaveBeenCalledWith(normalizedFile); }); @@ -470,10 +456,7 @@ describe("createDiskBackedLinterHost", () => { it("emits when watching directory is created", async () => { const host = createDiskBackedLinterHost(integrationRoot); const directoryPath = path.join(integrationRoot, "recreate-dir"); - const normalizedDirectory = normalizePath( - directoryPath, - host.isCaseSensitiveFS(), - ); + const normalizedDirectory = normalizePath(directoryPath); const onEvent = vi.fn(); using _ = host.watchDirectorySync(directoryPath, onEvent, { pollingInterval: 10, @@ -492,10 +475,7 @@ describe("createDiskBackedLinterHost", () => { it("emits when watching directory is deleted", async () => { const host = createDiskBackedLinterHost(integrationRoot); const directoryPath = path.join(integrationRoot, "recreate-dir"); - const normalizedDirectory = normalizePath( - directoryPath, - host.isCaseSensitiveFS(), - ); + const normalizedDirectory = normalizePath(directoryPath); const onEvent = vi.fn(); fs.mkdirSync(directoryPath, { recursive: true }); @@ -521,16 +501,10 @@ describe("createDiskBackedLinterHost", () => { const firstFile = path.join(directoryPath, "first.txt"); fs.writeFileSync(firstFile, "first"); - const normalizedFirst = normalizePath( - firstFile, - host.isCaseSensitiveFS(), - ); + const normalizedFirst = normalizePath(firstFile); const secondFile = path.join(directoryPath, "second.txt"); fs.writeFileSync(secondFile, "second"); - const normalizedSecond = normalizePath( - secondFile, - host.isCaseSensitiveFS(), - ); + const normalizedSecond = normalizePath(secondFile); using _ = host.watchDirectorySync(directoryPath, onEvent, { pollingInterval: 10, @@ -567,10 +541,7 @@ describe("createDiskBackedLinterHost", () => { const firstFile = path.join(directoryPath, "first.txt"); fs.writeFileSync(firstFile, "first"); - const normalizedFirst = normalizePath( - firstFile, - host.isCaseSensitiveFS(), - ); + const normalizedFirst = normalizePath(firstFile); await vi.waitFor(() => { expect(onEvent).toHaveBeenCalledWith(normalizedFirst); @@ -579,10 +550,7 @@ describe("createDiskBackedLinterHost", () => { fs.rmSync(directoryPath, { force: true, recursive: true }); - const normalizedDirectory = normalizePath( - directoryPath, - host.isCaseSensitiveFS(), - ); + const normalizedDirectory = normalizePath(directoryPath); await vi.waitFor(() => { expect(onEvent).toHaveBeenCalledWith(normalizedDirectory); }); @@ -597,10 +565,7 @@ describe("createDiskBackedLinterHost", () => { const secondFile = path.join(directoryPath, "second.txt"); fs.writeFileSync(secondFile, "second"); - const normalizedSecond = normalizePath( - secondFile, - host.isCaseSensitiveFS(), - ); + const normalizedSecond = normalizePath(secondFile); await vi.waitFor(() => { expect(onEvent).toHaveBeenCalledWith(normalizedSecond); }); @@ -608,14 +573,8 @@ describe("createDiskBackedLinterHost", () => { it("correctly reports when dir and its child have the same name", async () => { const host = createDiskBackedLinterHost(integrationRoot); - const directoryPath = normalizePath( - path.join(integrationRoot, "dir"), - host.isCaseSensitiveFS(), - ); - const subDirectoryPath = normalizePath( - path.join(directoryPath, "dir"), - host.isCaseSensitiveFS(), - ); + const directoryPath = normalizePath(path.join(integrationRoot, "dir")); + const subDirectoryPath = normalizePath(path.join(directoryPath, "dir")); const onEvent = vi.fn(); using _ = host.watchDirectorySync(directoryPath, onEvent, { pollingInterval: 10, diff --git a/packages/core/src/host/createDiskBackedLinterHost.ts b/packages/core/src/host/createDiskBackedLinterHost.ts index a51b7ac9e..798b21cca 100644 --- a/packages/core/src/host/createDiskBackedLinterHost.ts +++ b/packages/core/src/host/createDiskBackedLinterHost.ts @@ -1,3 +1,4 @@ +import { dirnameKey, normalizePath, pathKey } from "@flint.fyi/utils"; import fs from "node:fs"; import path from "node:path"; @@ -7,13 +8,12 @@ import type { LinterHostFileWatcherEvent, } from "../types/host.ts"; import { isFileSystemCaseSensitive } from "./isFileSystemCaseSensitive.ts"; -import { normalizePath } from "./normalizePath.ts"; const ignoredPaths = ["/node_modules", "/.git", "/.jj"]; export function createDiskBackedLinterHost(cwd: string): LinterHost { const caseSensitiveFS = isFileSystemCaseSensitive(); - cwd = normalizePath(cwd, caseSensitiveFS); + cwd = normalizePath(cwd); function createWatcher( normalizedWatchPath: string, @@ -35,7 +35,7 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { existsNow: boolean | null = null, ) { if (changedFileName != null) { - changedFileName = normalizePath(changedFileName, caseSensitiveFS); + changedFileName = normalizePath(changedFileName); } existsNow ??= fs.existsSync(normalizedWatchPath); if (existsNow) { @@ -80,7 +80,6 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { ) { changedPath = normalizePath( path.resolve(normalizedWatchPath, filename), - caseSensitiveFS, ); } if (statAndEmitIfChanged(changedPath)) { @@ -93,10 +92,7 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { statAndEmitIfChanged( filename == null ? null - : normalizePath( - path.resolve(normalizedWatchPath, filename), - caseSensitiveFS, - ), + : normalizePath(path.resolve(normalizedWatchPath, filename)), ) ) { return; @@ -253,10 +249,9 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { } }, watchDirectorySync(directoryPathAbsolute, callback, options) { - directoryPathAbsolute = normalizePath( - directoryPathAbsolute, - caseSensitiveFS, - ); + directoryPathAbsolute = normalizePath(directoryPathAbsolute); + const dirKey = pathKey(directoryPathAbsolute, caseSensitiveFS); + const dirKeySlash = dirnameKey(directoryPathAbsolute, caseSensitiveFS); return createWatcher( directoryPathAbsolute, @@ -264,9 +259,13 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { options.pollingInterval ?? 2_000, (normalizedChangedFilePath) => { normalizedChangedFilePath ??= directoryPathAbsolute; - if (normalizedChangedFilePath !== directoryPathAbsolute) { + const changedKey = pathKey( + normalizedChangedFilePath, + caseSensitiveFS, + ); + if (changedKey !== dirKey) { let relative = normalizedChangedFilePath; - if (relative.startsWith(directoryPathAbsolute + "/")) { + if (changedKey.startsWith(dirKeySlash)) { relative = relative.slice(directoryPathAbsolute.length); } for (const ignored of ignoredPaths) { @@ -283,14 +282,17 @@ export function createDiskBackedLinterHost(cwd: string): LinterHost { ); }, watchFileSync(filePathAbsolute, callback, options) { - filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); + const watchKey = pathKey(filePathAbsolute, caseSensitiveFS); return createWatcher( filePathAbsolute, false, options?.pollingInterval ?? 2_000, (normalizedChangedFilePath, event) => { - if (normalizedChangedFilePath === filePathAbsolute) { + if ( + normalizedChangedFilePath != null && + pathKey(normalizedChangedFilePath, caseSensitiveFS) === watchKey + ) { callback(event); } }, diff --git a/packages/core/src/host/createVFSLinterHost.test.ts b/packages/core/src/host/createVFSLinterHost.test.ts index 4f9f2809c..12de23887 100644 --- a/packages/core/src/host/createVFSLinterHost.test.ts +++ b/packages/core/src/host/createVFSLinterHost.test.ts @@ -13,16 +13,31 @@ describe(createVFSLinterHost, () => { expect(host.isCaseSensitiveFS()).toEqual(true); }); - it("normalizes cwd case-insensitively", () => { + it("normalizes cwd without lowercasing", () => { const host = createVFSLinterHost({ caseSensitive: false, cwd: "C:\\HELLO\\world\\", }); - expect(host.getCurrentDirectory()).toEqual("c:/hello/world"); + expect(host.getCurrentDirectory()).toEqual("C:/HELLO/world"); expect(host.isCaseSensitiveFS()).toEqual(false); }); + it("handles case-insensitive operations", () => { + const baseHost = createVFSLinterHost({ + caseSensitive: false, + cwd: "/root", + }); + const host = createVFSLinterHost({ baseHost }); + + host.vfsUpsertFile("/root/file.ts", "fake content"); + host.vfsUpsertFile("/root/FILE.ts", "real content"); + host.vfsUpsertFile("/root/otheR-File.ts", "other content"); + + expect(host.readFileSync("/root/file.ts")).toEqual("real content"); + expect(host.readFileSync("/root/OTHER-file.ts")).toEqual("other content"); + }); + it("inherits cwd and case sensitivity from base host", () => { const baseHost = createVFSLinterHost({ caseSensitive: true, @@ -494,7 +509,7 @@ describe(createVFSLinterHost, () => { }); host.vfsUpsertFile("C:\\file.txt", "content"); - expect(onEvent).toHaveBeenCalledExactlyOnceWith("c:/file.txt"); + expect(onEvent).toHaveBeenCalledExactlyOnceWith("C:/file.txt"); }); it("reports file editing", () => { diff --git a/packages/core/src/host/createVFSLinterHost.ts b/packages/core/src/host/createVFSLinterHost.ts index 93f418469..670b48ce0 100644 --- a/packages/core/src/host/createVFSLinterHost.ts +++ b/packages/core/src/host/createVFSLinterHost.ts @@ -1,3 +1,11 @@ +import { + dirnameKey, + normalizeDirname, + normalizePath, + pathKey, + type PathKey, +} from "@flint.fyi/utils"; + import type { LinterHost, LinterHostDirectoryEntry, @@ -7,7 +15,6 @@ import type { VFSLinterHost, } from "../types/host.ts"; import { isFileSystemCaseSensitive } from "./isFileSystemCaseSensitive.ts"; -import { normalizedDirname, normalizePath } from "./normalizePath.ts"; export type CreateVFSLinterHostOpts = | { @@ -45,45 +52,55 @@ export function createVFSLinterHost( let caseSensitiveFS: boolean; if (opts.baseHost == null) { caseSensitiveFS = opts.caseSensitive ?? isFileSystemCaseSensitive(); - cwd = normalizePath(opts.cwd, caseSensitiveFS); + cwd = normalizePath(opts.cwd); } else { baseHost = opts.baseHost; cwd = opts.cwd ?? baseHost.getCurrentDirectory(); caseSensitiveFS = baseHost.isCaseSensitiveFS(); } - const fileMap = new Map(); - const fileWatchers = new Map>(); - const directoryWatchers = new Map>(); + interface VfsFile { + content: string; + path: PathKey; + } + + const fileMap = new Map(); + const fileWatchers = new Map>(); + const directoryWatchers = new Map>(); const recursiveDirectoryWatchers = new Map< - string, + PathKey, Set >(); + function watchEvent( - normalizedFilePathAbsolute: string, + normalizedFilePathAbsolute: PathKey, fileEvent: LinterHostFileWatcherEvent, ) { for (const watcher of fileWatchers.get(normalizedFilePathAbsolute) ?? []) { watcher(fileEvent); } - let currentFile = normalizedFilePathAbsolute; - let currentDir = normalizedDirname(currentFile); + let currentFile: string = normalizedFilePathAbsolute; + let currentDir = normalizeDirname(currentFile); do { - for (const watcher of directoryWatchers.get(currentDir) ?? []) { + for (const watcher of directoryWatchers.get( + pathKey(currentDir, caseSensitiveFS), + ) ?? []) { watcher(currentFile); } currentFile = currentDir; - currentDir = normalizedDirname(currentFile); + currentDir = normalizeDirname(currentFile); } while (currentFile !== currentDir); - let dir = normalizedDirname(normalizedFilePathAbsolute); + let dir = normalizeDirname(normalizedFilePathAbsolute); while (true) { - for (const watcher of recursiveDirectoryWatchers.get(dir) ?? []) { + for (const watcher of recursiveDirectoryWatchers.get( + pathKey(dir, caseSensitiveFS), + ) ?? []) { watcher(normalizedFilePathAbsolute); } const prevDir = dir; - dir = normalizedDirname(dir); + dir = normalizeDirname(dir); if (prevDir === dir) { break; } @@ -91,12 +108,14 @@ export function createVFSLinterHost( } const host: VFSLinterHost = { fileTypeSync(pathAbsolute) { - pathAbsolute = normalizePath(pathAbsolute, caseSensitiveFS); - for (const filePath of fileMap.keys()) { - if (pathAbsolute === filePath) { + pathAbsolute = normalizePath(pathAbsolute); + const key = pathKey(pathAbsolute, caseSensitiveFS); + const keySlash = dirnameKey(pathAbsolute, caseSensitiveFS); + for (const fileKey of fileMap.keys()) { + if (key === fileKey) { return "file"; } - if (filePath.startsWith(pathAbsolute + "/")) { + if (fileKey.startsWith(keySlash)) { return "directory"; } } @@ -121,23 +140,24 @@ export function createVFSLinterHost( return host.readDirectorySync(directoryPathAbsolute); }, readDirectorySync(directoryPathAbsolute) { - directoryPathAbsolute = - normalizePath(directoryPathAbsolute, caseSensitiveFS) + "/"; + const dirNorm = normalizePath(directoryPathAbsolute); + const dirNormSlash = dirNorm.endsWith("/") ? dirNorm : dirNorm + "/"; + const dirKeySlash = dirnameKey(dirNorm, caseSensitiveFS); const result = new Map(); - for (let filePath of fileMap.keys()) { - if (!filePath.startsWith(directoryPathAbsolute)) { + for (const [fileKey, file] of fileMap) { + if (!fileKey.startsWith(dirKeySlash)) { continue; } - filePath = filePath.slice(directoryPathAbsolute.length); - const slashIndex = filePath.indexOf("/"); + const relPath = file.path.slice(dirNormSlash.length); + const slashIndex = relPath.indexOf("/"); let dirent: LinterHostDirectoryEntry = { - name: filePath, + name: relPath, type: "file", }; if (slashIndex >= 0) { dirent = { - name: filePath.slice(0, slashIndex), + name: relPath.slice(0, slashIndex), type: "directory", }; } @@ -151,7 +171,10 @@ export function createVFSLinterHost( ...(baseHost?.fileTypeSync(directoryPathAbsolute) === "directory" ? baseHost .readDirectorySync(directoryPathAbsolute) - .filter(({ name }) => !result.has(name)) + .filter( + ({ name }) => + !result.has(caseSensitiveFS ? name : name.toLowerCase()), + ) : []), ]; }, @@ -160,10 +183,10 @@ export function createVFSLinterHost( return host.readFileSync(filePathAbsolute); }, readFileSync(filePathAbsolute) { - filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); - const file = fileMap.get(filePathAbsolute); + filePathAbsolute = normalizePath(filePathAbsolute); + const file = fileMap.get(pathKey(filePathAbsolute, caseSensitiveFS)); if (file != null) { - return file; + return file.content; } if (baseHost?.fileTypeSync(filePathAbsolute) === "file") { return baseHost.readFileSync(filePathAbsolute); @@ -171,33 +194,38 @@ export function createVFSLinterHost( return undefined; }, vfsDeleteFile(filePathAbsolute) { - filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); - if (!fileMap.delete(filePathAbsolute)) { + filePathAbsolute = normalizePath(filePathAbsolute); + const key = pathKey(filePathAbsolute, caseSensitiveFS); + const file = fileMap.get(key); + if (file == null) { return; } - watchEvent(filePathAbsolute, "deleted"); + fileMap.delete(key); + watchEvent(file.path, "deleted"); }, vfsListFiles() { - return fileMap; + return new Map(Array.from(fileMap.values(), (f) => [f.path, f.content])); }, vfsUpsertFile(filePathAbsolute, content) { - filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); - const fileEvent = fileMap.has(filePathAbsolute) ? "changed" : "created"; - fileMap.set(filePathAbsolute, content); - watchEvent(filePathAbsolute, fileEvent); + filePathAbsolute = normalizePath(filePathAbsolute); + const key = pathKey(filePathAbsolute, caseSensitiveFS); + const existing = fileMap.get(key); + // TODO: Thread PathKey through the rest of the core. + const storedPath = existing?.path ?? (filePathAbsolute as PathKey); + const fileEvent = existing != null ? "changed" : "created"; + fileMap.set(key, { content, path: storedPath }); + watchEvent(storedPath, fileEvent); }, watchDirectorySync(directoryPathAbsolute, callback, options) { - directoryPathAbsolute = normalizePath( - directoryPathAbsolute, - caseSensitiveFS, - ); + directoryPathAbsolute = normalizePath(directoryPathAbsolute); + const key = pathKey(directoryPathAbsolute, caseSensitiveFS); const collection = options.recursive ? recursiveDirectoryWatchers : directoryWatchers; - let watchers = collection.get(directoryPathAbsolute); + let watchers = collection.get(key); if (watchers == null) { watchers = new Set(); - collection.set(directoryPathAbsolute, watchers); + collection.set(key, watchers); } watchers.add(callback); const baseWatcher = baseHost?.watchDirectorySync( @@ -209,18 +237,20 @@ export function createVFSLinterHost( [Symbol.dispose]() { watchers.delete(callback); if (!watchers.size) { - collection.delete(directoryPathAbsolute); + collection.delete(key); } baseWatcher?.[Symbol.dispose](); }, }; }, watchFileSync(filePathAbsolute, callback, options) { - filePathAbsolute = normalizePath(filePathAbsolute, caseSensitiveFS); - let watchers = fileWatchers.get(filePathAbsolute); + filePathAbsolute = normalizePath(filePathAbsolute); + const key = pathKey(filePathAbsolute, caseSensitiveFS); + let watchers = fileWatchers.get(key); + if (watchers == null) { watchers = new Set(); - fileWatchers.set(filePathAbsolute, watchers); + fileWatchers.set(key, watchers); } watchers.add(callback); const baseWatcher = baseHost?.watchFileSync( @@ -232,7 +262,7 @@ export function createVFSLinterHost( [Symbol.dispose]() { watchers.delete(callback); if (!watchers.size) { - fileWatchers.delete(filePathAbsolute); + fileWatchers.delete(key); } baseWatcher?.[Symbol.dispose](); }, diff --git a/packages/core/src/host/normalizePath.test.ts b/packages/core/src/host/normalizePath.test.ts deleted file mode 100644 index acef92145..000000000 --- a/packages/core/src/host/normalizePath.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { normalizedDirname, normalizePath } from "./normalizePath.ts"; - -describe("normalizePath", () => { - it("normalizes Windows path", () => { - const normalized = normalizePath("C:\\my-PATH\\foo\\", false); - - expect(normalized).toEqual("c:/my-path/foo"); - }); - - it("normalizes POSIX path", () => { - const normalized = normalizePath("/my-PATH/foo/", true); - - expect(normalized).toEqual("/my-PATH/foo"); - }); - - it("strips unnecessary path segments", () => { - const normalized = normalizePath("/foo//bar/../baz/.//", true); - - expect(normalized).toEqual("/foo/baz"); - }); - - it("doesn't strip root '/'", () => { - const normalized = normalizePath("/", true); - - expect(normalized).toEqual("/"); - }); - - it("doesn't strip root 'C:\\'", () => { - const normalized = normalizePath("C:\\", false); - - expect(normalized).toEqual("c:/"); - }); -}); - -describe("normalizedDirname", () => { - it("works with Windows path", () => { - const dirname = normalizedDirname("c:/foo/bar"); - - expect(dirname).toEqual("c:/foo"); - }); - - it("recognizes Windows root", () => { - const dirname = normalizedDirname("c:/foo"); - - expect(dirname).toEqual("c:/"); - }); - - it("recognizes bare Windows root", () => { - const dirname = normalizedDirname("c:/"); - - expect(dirname).toEqual("c:/"); - }); - - it("works with POSIX path", () => { - const dirname = normalizedDirname("/foo/bar"); - - expect(dirname).toEqual("/foo"); - }); - - it("recognizes POSIX root", () => { - const dirname = normalizedDirname("/foo"); - - expect(dirname).toEqual("/"); - }); - - it("recognizes bare POSIX root", () => { - const dirname = normalizedDirname("/"); - - expect(dirname).toEqual("/"); - }); -}); diff --git a/packages/core/src/host/normalizePath.ts b/packages/core/src/host/normalizePath.ts deleted file mode 100644 index 382e2d3df..000000000 --- a/packages/core/src/host/normalizePath.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { normalize } from "node:path"; - -export function normalizedDirname(path: string) { - const lastSlashIdx = path.lastIndexOf("/"); - path = path.slice(0, lastSlashIdx + 1); - if (path.indexOf("/") === lastSlashIdx && path.endsWith("/")) { - return path; - } - return path.slice(0, lastSlashIdx); -} - -export function normalizePath(path: string, caseSensitiveFS: boolean): string { - let result = normalize(path).replaceAll("\\", "/"); - if (result.indexOf("/") !== result.lastIndexOf("/") && result.endsWith("/")) { - result = result.slice(0, -1); - } - if (!caseSensitiveFS) { - result = result.toLowerCase(); - } - return result; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f9068bdec..7363f1717 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,7 +14,6 @@ export { type CreateVFSLinterHostOpts, } from "./host/createVFSLinterHost.ts"; export { isFileSystemCaseSensitive } from "./host/isFileSystemCaseSensitive.ts"; -export { normalizedDirname, normalizePath } from "./host/normalizePath.ts"; export { createLanguage } from "./languages/createLanguage.ts"; export { createPlugin } from "./plugins/createPlugin.ts"; export { formatReport } from "./reporting/formatReport.ts"; diff --git a/packages/core/src/running/computeUseDefinitions.ts b/packages/core/src/running/computeUseDefinitions.ts index 655936265..359667049 100644 --- a/packages/core/src/running/computeUseDefinitions.ts +++ b/packages/core/src/running/computeUseDefinitions.ts @@ -1,8 +1,8 @@ +import { normalizePath } from "@flint.fyi/utils"; import { debugForFile } from "debug-for-file"; import * as fs from "node:fs/promises"; import path from "node:path"; -import { normalizePath } from "../host/normalizePath.ts"; import type { ConfigRuleDefinition, ConfigUseDefinition, @@ -49,10 +49,7 @@ export async function computeUseDefinitions( ) .map((entry) => entry.isFile() - ? normalizePath( - path.join(entry.parentPath, entry.name), - host.isCaseSensitiveFS(), - ) + ? normalizePath(path.join(entry.parentPath, entry.name)) : null, ) .filter( diff --git a/packages/core/src/running/finalizeFileResults.ts b/packages/core/src/running/finalizeFileResults.ts index 7629aa94c..b26753cb5 100644 --- a/packages/core/src/running/finalizeFileResults.ts +++ b/packages/core/src/running/finalizeFileResults.ts @@ -1,9 +1,9 @@ +import { pathKey } from "@flint.fyi/utils"; import { debugForFile } from "debug-for-file"; import { resolve } from "node:path"; import { DirectivesFilterer } from "../directives/DirectivesFilterer.ts"; import { directiveReports } from "../directives/reports/directiveReports.ts"; -import { normalizePath } from "../host/normalizePath.ts"; import type { LinterHost } from "../types/host.ts"; import type { LanguageFileDiagnostic } from "../types/languages.ts"; import type { FileReport } from "../types/reports.ts"; @@ -39,7 +39,7 @@ export function finalizeFileResults( if (cache?.dependencies) { for (const dependency of cache.dependencies) { - const normalized = normalizePath( + const normalized = pathKey( resolve(dependency), host.isCaseSensitiveFS(), ); diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 5fd7d1c7c..a16c76372 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@flint.fyi/spelling": "workspace:^", "@flint.fyi/ts": "workspace:^", + "@flint.fyi/utils": "workspace:^", "execa": "9.6.1", "flint": "workspace:^", "vitest": "4.0.18", diff --git a/packages/e2e/tests/utils.ts b/packages/e2e/tests/utils.ts index 4a1211bfa..6b6fa43a9 100644 --- a/packages/e2e/tests/utils.ts +++ b/packages/e2e/tests/utils.ts @@ -1,14 +1,35 @@ +import { normalizePath } from "@flint.fyi/utils"; import { execa } from "execa"; +declare global { + // TODO[typescript>=6.0]: Remove this declaration. + // https://github.com/microsoft/TypeScript/pull/63046 + export interface RegExpConstructor { + /** + * Escapes any RegExp syntax characters in the input string, returning a + * new string that can be safely interpolated into a RegExp as a literal + * string to match. + * @example + * ```ts + * const regExp = new RegExp(RegExp.escape("foo.bar")); + * regExp.test("foo.bar"); // true + * regExp.test("foo!bar"); // false + * ``` + */ + escape(string: string): string; + } +} + /** * Normalizes CLI output for snapshot testing: converts backslashes to forward * slashes and replaces the given cwd with `<cwd>` so snapshots are portable. */ export function normalizeOutput(stdout: string, cwd: string): string { - const normalizedCwd = cwd.replace(/\\/g, "/"); + const normalizedCwd = normalizePath(cwd); + return stdout .replace(/\\/g, "/") - .replace(new RegExp(normalizedCwd, "gi"), "") + .replace(new RegExp(RegExp.escape(normalizedCwd), "gi"), "") .replace(/Finished in \S+/g, "Finished in