Add cline-ignore utility and tests

- Implemented `loadClineIgnoreFile` to read and return contents of `.clineignore`.
- Created `shouldIgnorePath` function to determine if a file path should be ignored based on patterns in the ignore file.
- Added tests for `shouldIgnorePath` covering exact matches, wildcards, directory patterns, comments, and negation patterns.
- Introduced new files: `src/utils/cline-ignore.ts` and `src/utils/__tests__/cline-ignore.test.ts`.

Enhance cline-ignore utility with additional functions and documentation

- Added `parseIgnorePatterns` function to streamline parsing of .clineignore file.
- Implemented `loadIgnorePatterns` to load patterns and provide an evaluation function.
- Introduced `filterIgnoredPaths` for batch filtering of file paths based on ignore patterns.
- Improved documentation with JSDoc comments for better clarity on function usage.
This commit is contained in:
HeavenOSK 2025-01-22 23:01:13 +09:00 committed by Matt Rubens
parent 7b668277fa
commit 5d19bbd52a
2 changed files with 160 additions and 0 deletions

View file

@ -0,0 +1,73 @@
import { shouldIgnorePath } from "../cline-ignore"
describe("shouldIgnorePath", () => {
test("exact match pattern", () => {
const ignoreContent = "test.txt"
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
expect(shouldIgnorePath("other.txt", ignoreContent)).toBe(false)
})
test("wildcard pattern", () => {
const ignoreContent = "*.txt"
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
expect(shouldIgnorePath("test.js", ignoreContent)).toBe(false)
})
test("directory pattern", () => {
const ignoreContent = "node_modules/"
expect(shouldIgnorePath("node_modules/package.json", ignoreContent)).toBe(true)
expect(shouldIgnorePath("src/node_modules.ts", ignoreContent)).toBe(false)
})
test("comments and empty lines", () => {
const ignoreContent = `
# This is a comment
test.txt
# This is also ignored
*.js
`
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
expect(shouldIgnorePath("app.js", ignoreContent)).toBe(true)
})
test("negation pattern", () => {
const ignoreContent = `
*.txt
!important.txt
docs/
!docs/README.txt
`
// Matches *.txt but excluded by !important.txt
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
expect(shouldIgnorePath("important.txt", ignoreContent)).toBe(false)
// Matches docs/ but excluded by !docs/README.txt
expect(shouldIgnorePath("docs/test.txt", ignoreContent)).toBe(true)
expect(shouldIgnorePath("docs/README.txt", ignoreContent)).toBe(false)
})
test("complex negation pattern combinations", () => {
const ignoreContent = `
# Ignore all .log files
*.log
# But not debug.log
!debug.log
# However, ignore debug.log in tmp/
tmp/debug.log
`
expect(shouldIgnorePath("error.log", ignoreContent)).toBe(true)
expect(shouldIgnorePath("debug.log", ignoreContent)).toBe(false)
expect(shouldIgnorePath("tmp/debug.log", ignoreContent)).toBe(true)
})
test("negation pattern with reversed order", () => {
const ignoreContent = `
!.env.example
.env*
`
// .env.example should be ignored because .env* comes after !.env.example
expect(shouldIgnorePath(".env.example", ignoreContent)).toBe(true)
expect(shouldIgnorePath(".env.local", ignoreContent)).toBe(true)
})
})

87
src/utils/cline-ignore.ts Normal file
View file

@ -0,0 +1,87 @@
import { fileExistsAtPath } from "./fs"
import * as path from "path"
import * as fs from "fs/promises"
/**
* Loads the contents of .clineignore file and returns cache and evaluation function.
* @param cwd Current working directory
* @returns Object containing patterns and evaluation function
*/
function parseIgnorePatterns(clineIgnoreFile: string): string[] {
return clineIgnoreFile
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
}
export async function loadIgnorePatterns(cwd: string): Promise<{
patterns: string[]
shouldIgnore: (path: string) => boolean
}> {
const ignoreContent = await loadClineIgnoreFile(cwd)
const patterns = parseIgnorePatterns(ignoreContent)
return {
patterns,
shouldIgnore: (path: string) => shouldIgnorePath(path, ignoreContent),
}
}
/**
* Filters multiple file paths in batch.
* @param paths Array of paths to filter
* @param ignoreContent Contents of .clineignore file
* @returns Array of filtered paths
*/
export function filterIgnoredPaths(paths: string[], ignoreContent: string): string[] {
return paths.filter((path) => !shouldIgnorePath(path, ignoreContent))
}
export async function loadClineIgnoreFile(cwd: string): Promise<string> {
const filePath = path.join(cwd, ".clineignore")
try {
const fileExists = await fileExistsAtPath(filePath)
if (!fileExists) {
return ""
}
return fs.readFile(filePath, "utf-8")
} catch (error) {
return ""
}
}
function convertGlobToRegExp(pattern: string): string {
// Handle directory pattern
if (pattern.endsWith("/")) {
pattern = pattern + "**"
}
return (
pattern
// Escape special characters
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
// Convert wildcard * to regex pattern
.replace(/\*/g, ".*")
)
}
export function shouldIgnorePath(filePath: string, clineIgnoreFile: string): boolean {
const patterns = parseIgnorePatterns(clineIgnoreFile)
let isIgnored = false
// Evaluate patterns in order
for (const pattern of patterns) {
const isNegation = pattern.startsWith("!")
const actualPattern = isNegation ? pattern.slice(1) : pattern
// Convert pattern to regex
const regexPattern = convertGlobToRegExp(actualPattern)
const regex = new RegExp(`^${regexPattern}$`)
// Check if pattern matches
if (regex.test(filePath)) {
isIgnored = !isNegation
}
}
return isIgnored
}