Skip to content

Commit e3d1404

Browse files
committed
fix(finder): resolve relative ignore patterns against scan dirs (#611)
Relative patterns like 'patches/**' or './ada/**' failed to match when fast-glob returned absolute result paths (absolute scan path) or when the ignore pattern was relative to a subdirectory being scanned (e.g. scanning './fixtures' with ignore './ada/**'). For each relative ignore pattern (not starting with '**/') generate additional variants resolved via path.join and path.resolve against every scan directory and cwd, covering both relative and absolute result paths. Patterns already starting with '**/' are left unchanged as they already work.
1 parent 5b1d3e2 commit e3d1404

2 files changed

Lines changed: 115 additions & 6 deletions

File tree

packages/finder/__tests__/files.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, afterEach } from 'vitest';
1+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
22
import * as path from 'path';
33
import * as os from 'os';
44
import * as fs from 'fs';
@@ -134,3 +134,76 @@ describe('getFilesToDetect — shebang detection', () => {
134134
});
135135
});
136136

137+
describe('getFilesToDetect — ignore patterns with relative paths (issue #611)', () => {
138+
let tmpDir: string;
139+
let originalCwd: string;
140+
141+
beforeEach(() => {
142+
originalCwd = process.cwd();
143+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jscpd-ignore-test-'));
144+
fs.mkdirSync(path.join(tmpDir, 'patches'), { recursive: true });
145+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
146+
// Each file needs enough lines to pass minLines filter
147+
const content = Array.from({ length: 10 }, (_, i) => `const v${i} = ${i};`).join('\n');
148+
fs.writeFileSync(path.join(tmpDir, 'patches', 'patch.js'), content);
149+
fs.writeFileSync(path.join(tmpDir, 'src', 'main.js'), content);
150+
process.chdir(tmpDir);
151+
});
152+
153+
afterEach(() => {
154+
process.chdir(originalCwd);
155+
fs.rmSync(tmpDir, { recursive: true, force: true });
156+
});
157+
158+
const makeOptions = (dir: string, ignore: string[]): any => ({
159+
path: [dir],
160+
format: ['javascript'],
161+
pattern: '**/*',
162+
minLines: 1,
163+
maxLines: 10000,
164+
maxSize: '100mb',
165+
ignore,
166+
noSymlinks: false,
167+
absolute: false,
168+
});
169+
170+
it('relative ignore pattern "patches/**" works when scan path is absolute (issue #611)', () => {
171+
// Simulate the exact issue #611 scenario: default path=[process.cwd()] is absolute,
172+
// and relative ignore patterns must still work.
173+
const files = getFilesToDetect(makeOptions(process.cwd(), ['patches/**']));
174+
const filePaths = files.map(f => f.path);
175+
expect(filePaths.some(p => p.includes('patches'))).toBe(false);
176+
expect(filePaths.some(p => p.includes('src'))).toBe(true);
177+
});
178+
179+
it('relative ignore pattern "./patches/**" works when scan path is absolute', () => {
180+
const files = getFilesToDetect(makeOptions(process.cwd(), ['./patches/**']));
181+
const filePaths = files.map(f => f.path);
182+
expect(filePaths.some(p => p.includes('patches'))).toBe(false);
183+
expect(filePaths.some(p => p.includes('src'))).toBe(true);
184+
});
185+
186+
it('relative ignore pattern "patches/**" works when scan path is "." (relative)', () => {
187+
const files = getFilesToDetect(makeOptions('.', ['patches/**']));
188+
const filePaths = files.map(f => f.path);
189+
expect(filePaths.some(p => p.includes('patches'))).toBe(false);
190+
expect(filePaths.some(p => p.includes('src'))).toBe(true);
191+
});
192+
193+
it('relative ignore pattern "./ada/**" works when scanning a subdirectory (issue #611)', () => {
194+
// Create a sub-fixture: tmpDir/subdir/{ada,src}
195+
fs.mkdirSync(path.join(tmpDir, 'subdir', 'ada'), { recursive: true });
196+
fs.mkdirSync(path.join(tmpDir, 'subdir', 'src'), { recursive: true });
197+
const content = Array.from({ length: 10 }, (_, i) => `const v${i} = ${i};`).join('\n');
198+
fs.writeFileSync(path.join(tmpDir, 'subdir', 'ada', 'ada.js'), content);
199+
fs.writeFileSync(path.join(tmpDir, 'subdir', 'src', 'main.js'), content);
200+
201+
// Scan path is a relative subdirectory; user writes "./ada/**" meaning
202+
// "ada within what I am scanning", not "ada at cwd level".
203+
const files = getFilesToDetect(makeOptions('./subdir', ['./ada/**']));
204+
const filePaths = files.map(f => f.path);
205+
expect(filePaths.some(p => p.includes('ada'))).toBe(false);
206+
expect(filePaths.some(p => p.includes('src'))).toBe(true);
207+
});
208+
});
209+

packages/finder/src/files.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,25 +149,61 @@ function addContentToEntry(entry: IEntry): EntryWithContent {
149149
export function getFilesToDetect(options: IOptions): EntryWithContent[] {
150150
const pattern = options.pattern || '**/*';
151151
let patterns = options.path;
152+
const cwd = process.cwd();
152153

153154
if (options.noSymlinks) {
154155
patterns = patterns!==undefined ? patterns.filter((path: string) => !isSymlink(path)) : [];
155156
}
156157

157-
patterns = patterns!==undefined ? patterns.map((path: string) => {
158-
const currentPath = realpathSync(path);
158+
// Capture scan directories before appending the glob pattern, so we can resolve
159+
// ignore patterns relative to each scan directory below.
160+
const scanDirs: string[] = (patterns || []).map((inputPath: string) => {
161+
try {
162+
return isFile(realpathSync(inputPath)) ? path.dirname(inputPath) : inputPath;
163+
} catch {
164+
return inputPath;
165+
}
166+
});
167+
168+
patterns = patterns!==undefined ? patterns.map((inputPath: string) => {
169+
const currentPath = realpathSync(inputPath);
159170

160171
if (isFile(currentPath)) {
161-
return path;
172+
return inputPath;
162173
}
163174

164-
return path.endsWith('/') ? `${path}${pattern}` : `${path}/${pattern}`;
175+
return inputPath.endsWith('/') ? `${inputPath}${pattern}` : `${inputPath}/${pattern}`;
165176
}): [];
166177

178+
// Normalize ignore patterns so they work regardless of whether the scan path
179+
// is relative or absolute and regardless of whether it equals cwd (issue #611).
180+
//
181+
// fast-glob returns relative result paths for relative scan patterns and
182+
// absolute result paths for absolute patterns. A pattern like "./ada/**" won't
183+
// match either "fixtures/ada/file.js" (relative, when scanning "./fixtures")
184+
// or "/cwd/fixtures/ada/file.js" (absolute, when scanning an absolute path).
185+
//
186+
// For each relative ignore pattern we generate additional variants:
187+
// • original – keeps backward-compat for trivial cases
188+
// • path.join(scanDir, pattern) – matches relative result paths
189+
// • path.resolve(cwd, scanDir, pattern) – matches absolute result paths
190+
// Patterns already starting with "**/" already work and are left unchanged.
191+
const normalizedIgnore = (options.ignore || []).flatMap((ignorePattern: string) => {
192+
if (path.isAbsolute(ignorePattern) || ignorePattern.startsWith('**/')) {
193+
return [ignorePattern];
194+
}
195+
const variants = new Set<string>([ignorePattern]);
196+
for (const scanDir of [...scanDirs, '.']) {
197+
variants.add(path.join(scanDir, ignorePattern));
198+
variants.add(path.resolve(cwd, scanDir, ignorePattern));
199+
}
200+
return [...variants];
201+
});
202+
167203
return (sync(
168204
patterns,
169205
{
170-
ignore: options.ignore,
206+
ignore: normalizedIgnore,
171207
onlyFiles: true,
172208
dot: true,
173209
stats: true,

0 commit comments

Comments
 (0)