fix: centralize absolute path detection

This commit is contained in:
iliya 2026-05-16 18:21:14 +03:00
parent a6ba6072c0
commit 94fd9d1259
5 changed files with 45 additions and 52 deletions

View file

@ -2,7 +2,7 @@
* Path resolution utilities for the store.
*/
import { stripTrailingSeparators } from '@shared/utils/platformPath';
import { isAbsoluteOrHomePath, stripTrailingSeparators } from '@shared/utils/platformPath';
/**
* Resolves a relative path against a base path, handling various path formats.
@ -15,7 +15,7 @@ import { stripTrailingSeparators } from '@shared/utils/platformPath';
*/
export function resolveFilePath(base: string, relativePath: string): string {
// If already absolute, return as-is
if (isAbsolutePath(relativePath)) {
if (isAbsoluteOrHomePath(relativePath)) {
return relativePath;
}
@ -27,13 +27,7 @@ export function resolveFilePath(base: string, relativePath: string): string {
cleanRelative = cleanRelative.slice(1);
}
if (isAbsolutePath(cleanRelative)) {
return cleanRelative;
}
// Tilde paths (~/) are home-relative absolute paths - pass through as-is
// The main process will expand ~ to the actual home directory
if (cleanRelative.startsWith('~/') || cleanRelative.startsWith('~\\') || cleanRelative === '~') {
if (isAbsoluteOrHomePath(cleanRelative)) {
return cleanRelative;
}
@ -69,10 +63,6 @@ export function resolveFilePath(base: string, relativePath: string): string {
return remainingRelative ? `${normalizedBase}${separator}${remainingRelative}` : normalizedBase;
}
function isAbsolutePath(input: string): boolean {
return input.startsWith('/') || input.startsWith('\\\\') || /^[a-zA-Z]:[\\/]/.test(input);
}
function normalizeSeparators(input: string, separator: '/' | '\\'): string {
let output = '';
let prevWasSeparator = false;

View file

@ -8,6 +8,7 @@
*/
import {
isAbsoluteOrHomePath,
isPathPrefix,
lastSeparatorIndex,
normalizePathForComparison,
@ -63,20 +64,6 @@ export function getDisplayName(path: string, _source: ClaudeMdSource): string {
return path;
}
/**
* Check if a path is absolute (starts with /).
*/
function isAbsolutePath(path: string): boolean {
return (
path.startsWith('/') ||
path.startsWith('~/') ||
path.startsWith('~\\') ||
path === '~' ||
path.startsWith('\\\\') ||
/^[a-zA-Z]:[\\/]/.test(path)
);
}
/**
* Join paths, handling various path formats properly.
* Handles:
@ -87,7 +74,7 @@ function isAbsolutePath(path: string): boolean {
* - Paths with @ prefix: @apps/foo/bar.tsx (strips @ then joins)
*/
function joinPaths(base: string, relative: string): string {
if (isAbsolutePath(relative)) {
if (isAbsoluteOrHomePath(relative)) {
return relative;
}
@ -99,7 +86,7 @@ function joinPaths(base: string, relative: string): string {
if (cleanRelative.startsWith('@')) {
cleanRelative = cleanRelative.slice(1);
}
if (isAbsolutePath(cleanRelative)) {
if (isAbsoluteOrHomePath(cleanRelative)) {
return cleanRelative;
}
@ -239,7 +226,9 @@ export function extractUserMentionPaths(
for (const ref of fileReferences) {
if (ref.path) {
// Convert to absolute if relative
const absolutePath = isAbsolutePath(ref.path) ? ref.path : joinPaths(projectRoot, ref.path);
const absolutePath = isAbsoluteOrHomePath(ref.path)
? ref.path
: joinPaths(projectRoot, ref.path);
paths.push(absolutePath);
}
}
@ -525,7 +514,7 @@ function computeClaudeMdStats(params: ComputeClaudeMdStatsParams): ClaudeMdStats
const responseRefs = extractFileRefsFromResponses(aiGroup.responses);
for (const ref of responseRefs) {
if (ref.path) {
const absPath = isAbsolutePath(ref.path) ? ref.path : joinPaths(projectRoot, ref.path);
const absPath = isAbsoluteOrHomePath(ref.path) ? ref.path : joinPaths(projectRoot, ref.path);
allFilePaths.push(absPath);
}
}

View file

@ -9,7 +9,11 @@
* This builds on claudeMdTracker.ts and extends it to track all context sources.
*/
import { normalizePathForComparison, stripTrailingSeparators } from '@shared/utils/platformPath';
import {
isAbsoluteOrHomePath,
normalizePathForComparison,
stripTrailingSeparators,
} from '@shared/utils/platformPath';
import { estimateTokens } from '@shared/utils/tokenFormatting';
import { MAX_MENTIONED_FILE_TOKENS } from '../types/contextInjection';
@ -447,20 +451,6 @@ interface ComputeContextStatsParams {
directoryTokenData?: Record<string, ClaudeMdFileInfo>;
}
/**
* Helper to check if a path is absolute.
*/
function isAbsolutePath(path: string): boolean {
return (
path.startsWith('/') ||
path.startsWith('~/') ||
path.startsWith('~\\') ||
path === '~' ||
path.startsWith('\\\\') ||
/^[a-zA-Z]:[\\/]/.test(path)
);
}
/**
* Helper to join paths, handling various path formats properly.
* Handles:
@ -471,7 +461,7 @@ function isAbsolutePath(path: string): boolean {
* - Paths with @ prefix: @apps/foo/bar.tsx (strips @ then joins)
*/
function joinPaths(base: string, relative: string): string {
if (isAbsolutePath(relative)) {
if (isAbsoluteOrHomePath(relative)) {
return relative;
}
@ -482,7 +472,7 @@ function joinPaths(base: string, relative: string): string {
if (cleanRelative.startsWith('@')) {
cleanRelative = cleanRelative.slice(1);
}
if (isAbsolutePath(cleanRelative)) {
if (isAbsoluteOrHomePath(cleanRelative)) {
return cleanRelative;
}
@ -679,7 +669,7 @@ function computeContextStats(params: ComputeContextStatsParams): ContextStats {
const responseRefs = extractFileRefsFromResponses(aiGroup.responses);
for (const ref of responseRefs) {
if (ref.path) {
const absPath = isAbsolutePath(ref.path) ? ref.path : joinPaths(projectRoot, ref.path);
const absPath = isAbsoluteOrHomePath(ref.path) ? ref.path : joinPaths(projectRoot, ref.path);
allFilePaths.push(absPath);
}
}
@ -735,7 +725,7 @@ function computeContextStats(params: ComputeContextStatsParams): ContextStats {
if (!fileRef.path) continue;
// Convert to absolute path if needed
const absolutePath = isAbsolutePath(fileRef.path)
const absolutePath = isAbsoluteOrHomePath(fileRef.path)
? fileRef.path
: joinPaths(projectRoot, fileRef.path);
@ -768,7 +758,7 @@ function computeContextStats(params: ComputeContextStatsParams): ContextStats {
for (const fileRef of responseRefs) {
if (!fileRef.path) continue;
const absolutePath = isAbsolutePath(fileRef.path)
const absolutePath = isAbsoluteOrHomePath(fileRef.path)
? fileRef.path
: joinPaths(projectRoot, fileRef.path);

View file

@ -22,6 +22,18 @@ export function isWindowsishPath(filePath: string): boolean {
return /^[A-Za-z]:\//.test(p) || p.startsWith('//');
}
/** True for filesystem-absolute paths and home-relative `~` paths. */
export function isAbsoluteOrHomePath(filePath: string): boolean {
return (
filePath.startsWith('/') ||
filePath.startsWith('~/') ||
filePath.startsWith('~\\') ||
filePath === '~' ||
filePath.startsWith('\\\\') ||
/^[A-Za-z]:[\\/]/.test(filePath)
);
}
/**
* Normalize for comparisons:
* - Convert `\``/`

View file

@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest';
import { getRelativePathWithinPrefix, isPathPrefix } from '../../../src/shared/utils/platformPath';
import {
getRelativePathWithinPrefix,
isAbsoluteOrHomePath,
isPathPrefix,
} from '../../../src/shared/utils/platformPath';
describe('platformPath Windows containment', () => {
it('matches Windows drive paths case-insensitively and preserves child path style', () => {
@ -35,4 +39,12 @@ describe('platformPath Windows containment', () => {
expect(isPathPrefix('', '/Users/Alice/Repo/src/app.ts')).toBe(false);
expect(getRelativePathWithinPrefix('', '/Users/Alice/Repo/src/app.ts')).toBe(null);
});
it('detects absolute and home-relative paths across platforms', () => {
expect(isAbsoluteOrHomePath('/Users/Alice/Repo')).toBe(true);
expect(isAbsoluteOrHomePath('C:\\Users\\Alice\\Repo')).toBe(true);
expect(isAbsoluteOrHomePath('\\\\server\\share\\Repo')).toBe(true);
expect(isAbsoluteOrHomePath('~/Repo')).toBe(true);
expect(isAbsoluteOrHomePath('src/app.ts')).toBe(false);
});
});