diff --git a/src/const.ts b/src/const.ts index 40089a0..a114416 100644 --- a/src/const.ts +++ b/src/const.ts @@ -34,7 +34,7 @@ export const DEFAULT_EXCLUDED_PATH = [ "webpack.config.js", "**/*.min.js", "tsconfig.js", - "**.test.**", + "**/*.test.*", "tests", ]; diff --git a/src/fs/glob.ts b/src/fs/glob.ts index 2cfb446..e98b163 100644 --- a/src/fs/glob.ts +++ b/src/fs/glob.ts @@ -38,25 +38,23 @@ export function getParser( } } -// Build the ignore function for Glob -export const ignoreFunc = ( - filePath: Path, - excludedPatterns: string[], -): boolean => { - return excludedPatterns.some((exclude) => { +/** + * Classify exclude patterns into directory names (for tree pruning) + * and file/glob patterns (for per-file filtering). + */ +export function classifyExcludes(excludedPatterns: string[]) { + const dirs: string[] = []; + const filePatterns: string[] = []; + for (const exclude of excludedPatterns) { const type = detectPatternType(exclude); - // return true to ignore - switch (type) { - case "file": - return filePath.isNamed(exclude); - case "directory": - return filePath.relative().includes(exclude); - default: - // Handle glob patterns using minimatch - return minimatch(filePath.relative(), exclude); + if (type === "directory") { + dirs.push(exclude); + } else { + filePatterns.push(exclude); } - }) as boolean; -}; + } + return { dirs, filePatterns }; +} /** * Retrieves a list of files based on the provided arguments and patterns. @@ -66,16 +64,26 @@ export const ignoreFunc = ( * @return A promise that resolves to an array of file paths. */ export async function getFiles(args: Args, pattern: Patterns): Promise { - // 1. Create the Glob instance + const { dirs, filePatterns } = classifyExcludes(pattern.exclude); + const g = new Glob(pattern.include, { ignore: { - ignored: (p: Path) => ignoreFunc(p, pattern.exclude), + // Prune entire directory subtrees — glob won't enter these dirs at all + childrenIgnored: (p: Path) => dirs.some((d) => p.isNamed(d)), + // Filter individual files by name or glob pattern + ignored: (p: Path) => + filePatterns.some((fp) => { + const type = detectPatternType(fp); + if (type === "file") { + return p.isNamed(fp); + } + return minimatch(p.relative(), fp); + }), }, nodir: true, cwd: args.paths.cwd, root: args.paths.root ? path.resolve(args.paths.root) : undefined, }); - // 2. Return the walk() promise, which resolves to an array of all file paths return g.walk(); } diff --git a/src/parser/tree.ts b/src/parser/tree.ts index f042440..d37f651 100644 --- a/src/parser/tree.ts +++ b/src/parser/tree.ts @@ -29,6 +29,24 @@ function collectComments(node: SyntaxNode): string | undefined { return undefined; } +/** + * Map of escape characters to their resolved values. + * Used by both PHP (encapsed_string) and JS (string) handlers. + */ +const escapeMap: Record = { + 'n': '\n', + 'r': '\r', + 't': '\t', + 'f': '\f', + 'v': '\v', + '0': '\0', + '\\': '\\', + '"': '"', + "'": "'", + '$': '$', + 'e': '\x1b', +}; + /** * Resolves the actual string value from a tree-sitter node, * handling escape sequences in double-quoted strings. @@ -42,19 +60,9 @@ function resolveStringValue(node: SyntaxNode): string { return node.children .map((child) => { if (child.type === 'escape_sequence') { - // Unescape common sequences - switch (child.text) { - case '\\n': return '\n'; - case '\\r': return '\r'; - case '\\t': return '\t'; - case '\\\\': return '\\'; - case '\\"': return '"'; - case '\\$': return '$'; - case '\\e': return '\x1b'; - case '\\f': return '\f'; - case '\\v': return '\v'; - default: return child.text; - } + // child.text is e.g. "\\n" — the char after the backslash is the key + const ch = child.text.slice(1); + return ch in escapeMap ? escapeMap[ch] : child.text; } // Return literal content if (child.type === 'string_content') { @@ -75,8 +83,18 @@ function resolveStringValue(node: SyntaxNode): string { // Strip surrounding quotes if present if ((text.startsWith("'") && text.endsWith("'")) || (text.startsWith('"') && text.endsWith('"'))) { - // Remove quotes and unescape escaped quotes - return text.slice(1, -1).replace(/\\'/g, "'").replace(/\\\\/g, "\\"); + const isDouble = text.startsWith('"'); + let inner = text.slice(1, -1); + if (isDouble) { + // Unescape common escape sequences for double-quoted strings + inner = inner.replace(/\\(.)/g, (_match, ch) => + ch in escapeMap ? escapeMap[ch] : _match + ); + } else { + // Single-quoted: only unescape \\ and \' + inner = inner.replace(/\\'/g, "'").replace(/\\\\/g, "\\"); + } + return inner; } } diff --git a/src/utils/common.ts b/src/utils/common.ts index 6d6b303..ef52ed6 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -63,7 +63,7 @@ export function detectPatternType( pattern: string, ): "file" | "directory" | "glob" { const containsFileExtension = pattern.includes("."); - const containsDirectorySeparator = pattern.includes(path.sep); + const containsDirectorySeparator = pattern.includes("/"); if (pattern.includes("*")) { return "glob";