Skip to content

Commit a9a3ed5

Browse files
authored
Merge pull request #84 from wp-blocks/centralize-escape-sequence-handling
Centralize escape sequence handling
2 parents 665d77d + 04172cc commit a9a3ed5

4 files changed

Lines changed: 63 additions & 37 deletions

File tree

src/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const DEFAULT_EXCLUDED_PATH = [
3434
"webpack.config.js",
3535
"**/*.min.js",
3636
"tsconfig.js",
37-
"**.test.**",
37+
"**/*.test.*",
3838
"tests",
3939
];
4040

src/fs/glob.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,23 @@ export function getParser(
3838
}
3939
}
4040

41-
// Build the ignore function for Glob
42-
export const ignoreFunc = (
43-
filePath: Path,
44-
excludedPatterns: string[],
45-
): boolean => {
46-
return excludedPatterns.some((exclude) => {
41+
/**
42+
* Classify exclude patterns into directory names (for tree pruning)
43+
* and file/glob patterns (for per-file filtering).
44+
*/
45+
export function classifyExcludes(excludedPatterns: string[]) {
46+
const dirs: string[] = [];
47+
const filePatterns: string[] = [];
48+
for (const exclude of excludedPatterns) {
4749
const type = detectPatternType(exclude);
48-
// return true to ignore
49-
switch (type) {
50-
case "file":
51-
return filePath.isNamed(exclude);
52-
case "directory":
53-
return filePath.relative().includes(exclude);
54-
default:
55-
// Handle glob patterns using minimatch
56-
return minimatch(filePath.relative(), exclude);
50+
if (type === "directory") {
51+
dirs.push(exclude);
52+
} else {
53+
filePatterns.push(exclude);
5754
}
58-
}) as boolean;
59-
};
55+
}
56+
return { dirs, filePatterns };
57+
}
6058

6159
/**
6260
* Retrieves a list of files based on the provided arguments and patterns.
@@ -66,16 +64,26 @@ export const ignoreFunc = (
6664
* @return A promise that resolves to an array of file paths.
6765
*/
6866
export async function getFiles(args: Args, pattern: Patterns): Promise<string[]> {
69-
// 1. Create the Glob instance
67+
const { dirs, filePatterns } = classifyExcludes(pattern.exclude);
68+
7069
const g = new Glob(pattern.include, {
7170
ignore: {
72-
ignored: (p: Path) => ignoreFunc(p, pattern.exclude),
71+
// Prune entire directory subtrees — glob won't enter these dirs at all
72+
childrenIgnored: (p: Path) => dirs.some((d) => p.isNamed(d)),
73+
// Filter individual files by name or glob pattern
74+
ignored: (p: Path) =>
75+
filePatterns.some((fp) => {
76+
const type = detectPatternType(fp);
77+
if (type === "file") {
78+
return p.isNamed(fp);
79+
}
80+
return minimatch(p.relative(), fp);
81+
}),
7382
},
7483
nodir: true,
7584
cwd: args.paths.cwd,
7685
root: args.paths.root ? path.resolve(args.paths.root) : undefined,
7786
});
7887

79-
// 2. Return the walk() promise, which resolves to an array of all file paths
8088
return g.walk();
8189
}

src/parser/tree.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ function collectComments(node: SyntaxNode): string | undefined {
2929
return undefined;
3030
}
3131

32+
/**
33+
* Map of escape characters to their resolved values.
34+
* Used by both PHP (encapsed_string) and JS (string) handlers.
35+
*/
36+
const escapeMap: Record<string, string> = {
37+
'n': '\n',
38+
'r': '\r',
39+
't': '\t',
40+
'f': '\f',
41+
'v': '\v',
42+
'0': '\0',
43+
'\\': '\\',
44+
'"': '"',
45+
"'": "'",
46+
'$': '$',
47+
'e': '\x1b',
48+
};
49+
3250
/**
3351
* Resolves the actual string value from a tree-sitter node,
3452
* handling escape sequences in double-quoted strings.
@@ -42,19 +60,9 @@ function resolveStringValue(node: SyntaxNode): string {
4260
return node.children
4361
.map((child) => {
4462
if (child.type === 'escape_sequence') {
45-
// Unescape common sequences
46-
switch (child.text) {
47-
case '\\n': return '\n';
48-
case '\\r': return '\r';
49-
case '\\t': return '\t';
50-
case '\\\\': return '\\';
51-
case '\\"': return '"';
52-
case '\\$': return '$';
53-
case '\\e': return '\x1b';
54-
case '\\f': return '\f';
55-
case '\\v': return '\v';
56-
default: return child.text;
57-
}
63+
// child.text is e.g. "\\n" — the char after the backslash is the key
64+
const ch = child.text.slice(1);
65+
return ch in escapeMap ? escapeMap[ch] : child.text;
5866
}
5967
// Return literal content
6068
if (child.type === 'string_content') {
@@ -75,8 +83,18 @@ function resolveStringValue(node: SyntaxNode): string {
7583
// Strip surrounding quotes if present
7684
if ((text.startsWith("'") && text.endsWith("'")) ||
7785
(text.startsWith('"') && text.endsWith('"'))) {
78-
// Remove quotes and unescape escaped quotes
79-
return text.slice(1, -1).replace(/\\'/g, "'").replace(/\\\\/g, "\\");
86+
const isDouble = text.startsWith('"');
87+
let inner = text.slice(1, -1);
88+
if (isDouble) {
89+
// Unescape common escape sequences for double-quoted strings
90+
inner = inner.replace(/\\(.)/g, (_match, ch) =>
91+
ch in escapeMap ? escapeMap[ch] : _match
92+
);
93+
} else {
94+
// Single-quoted: only unescape \\ and \'
95+
inner = inner.replace(/\\'/g, "'").replace(/\\\\/g, "\\");
96+
}
97+
return inner;
8098
}
8199
}
82100

src/utils/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function detectPatternType(
6363
pattern: string,
6464
): "file" | "directory" | "glob" {
6565
const containsFileExtension = pattern.includes(".");
66-
const containsDirectorySeparator = pattern.includes(path.sep);
66+
const containsDirectorySeparator = pattern.includes("/");
6767

6868
if (pattern.includes("*")) {
6969
return "glob";

0 commit comments

Comments
 (0)