472aa4aaed1aeaa77aeb82fa9b6c55d7efa0eb04

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Fix false filesystem search detection for piped rg

Diff

  1diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
  2index 36f4fd006963eb9349d4fa8fe982209eb54e8c73..fbeeeceb26bb571b59947eb66aa65ee934852d2a 100644
  3--- a/src/core/deterministic.ts
  4+++ b/src/core/deterministic.ts
  5@@ -312,7 +312,7 @@ function safeFind(args: string[]): boolean {
  6 
  7 type ArgumentRoles = { paths: string[]; searchRoots?: string[]; review?: boolean };
  8 
  9-function searchPaths(program: string, args: string[]): ArgumentRoles {
 10+function searchPaths(program: string, args: string[], pipedInput: boolean): ArgumentRoles {
 11   const paths: string[] = [];
 12   const positional: string[] = [];
 13   const expressionOptions = new Set(["-e", "--regexp", "-f", "--file"]);
 14@@ -439,12 +439,15 @@ function searchPaths(program: string, args: string[]): ArgumentRoles {
 15   }
 16   const roots = expression || (program === "rg" && files) ? positional : positional.slice(1);
 17   paths.push(...roots);
 18-  return { paths, searchRoots: recursive ? (roots.length ? roots : ["."]) : undefined };
 19+  return {
 20+    paths,
 21+    searchRoots: recursive ? (roots.length ? roots : pipedInput && program === "rg" && !files ? [] : ["."]) : undefined,
 22+  };
 23 }
 24 
 25-function argumentRoles(program: string, args: string[]): ArgumentRoles {
 26+function argumentRoles(program: string, args: string[], pipedInput: boolean): ArgumentRoles {
 27   if (["echo", "printf", "dirname", "tr"].includes(program)) return { paths: [] };
 28-  if (program === "grep" || program === "rg") return searchPaths(program, args);
 29+  if (program === "grep" || program === "rg") return searchPaths(program, args, pipedInput);
 30   if (program === "find") {
 31     const roots = findRoots(args);
 32     return { paths: roots, searchRoots: roots.length ? roots : ["."] };
 33@@ -601,7 +604,7 @@ export type BashDeterministicResult = {
 34   reviewCommands?: string[];
 35 };
 36 
 37-function checkCommand(command: BashCommand, requestCwd: string): BashDeterministicResult {
 38+function checkCommand(command: BashCommand, requestCwd: string, pipedInput: boolean): BashDeterministicResult {
 39   const unknown: BashDeterministicResult = { reviewCommands: [command.source] };
 40   const literalWords = command.words.map((word) => word.text);
 41   const literalDanger = dangerousCommand(literalWords[0], literalWords.slice(1));
 42@@ -631,7 +634,7 @@ function checkCommand(command: BashCommand, requestCwd: string): BashDeterminist
 43   if (!program) return unknown;
 44   const dangerous = dangerousCommand(program, args);
 45   if (dangerous) return { ...unknown, decision: dangerous };
 46-  const roles = argumentRoles(program, args);
 47+  const roles = argumentRoles(program, args, pipedInput);
 48   let cwd = requestCwd;
 49   if (program === "git") {
 50     const operationIndex = gitOperationIndex(args);
 51@@ -727,12 +730,12 @@ function combine(left: Decision | undefined, right: Decision | undefined): Decis
 52   return left && right ? left : undefined;
 53 }
 54 
 55-function evaluateNode(node: BashNode, cwd: string): BashDeterministicResult {
 56+function evaluateNode(node: BashNode, cwd: string, pipedInput = false): BashDeterministicResult {
 57   if (node.kind === "unsupported") return {};
 58-  if (node.kind === "command") return checkCommand(node.command, cwd);
 59-  if (node.kind === "scope") return evaluateNode(node.body, cwd);
 60-  const left = evaluateNode(node.left, cwd);
 61-  const right = evaluateNode(node.right, cwd);
 62+  if (node.kind === "command") return checkCommand(node.command, cwd, pipedInput);
 63+  if (node.kind === "scope") return evaluateNode(node.body, cwd, pipedInput);
 64+  const left = evaluateNode(node.left, cwd, pipedInput);
 65+  const right = evaluateNode(node.right, cwd, pipedInput || node.operator === "|");
 66   return {
 67     decision: combine(left.decision, right.decision),
 68     reviewCommands: combinedReviewCommands(left, right),
 69diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
 70index 8ee15d8231cb2468b86f1c32dfbeaba3db21b415..96ce003773f4a2b8216919570186995696e9d062 100644
 71--- a/test/core/deterministic.test.ts
 72+++ b/test/core/deterministic.test.ts
 73@@ -146,6 +146,10 @@ test.each([
 74   "find root-link -name '*.md'",
 75   "find home-link -name '*.md'",
 76   "rg needle /",
 77+  "echo ok | rg needle /",
 78+  "echo ok | rg --files ~",
 79+  "echo ok | grep -r needle /",
 80+  "echo ok; rg needle /",
 81   "rg --files ~",
 82   "rg -e needle ~",
 83   "rg needle home-link",
 84@@ -175,6 +179,29 @@ test.each(["grep -e -r README.md", "grep -- -r README.md", "grep needle"])(
 85   },
 86 );
 87 
 88+test.each([
 89+  "echo ok | rg -n -A45 -B4 '^#.*bell-features|bell-features ='",
 90+  "echo ok | rg needle | rg other",
 91+  "echo ok | (rg needle)",
 92+])("allows piped rg input at filesystem roots: %s", async (command) => {
 93+  for (const directory of ["/", process.env.HOME!]) {
 94+    expect(await checkDeterministic("bash", { command }, directory)).toMatchObject({ decision: "allow" });
 95+  }
 96+});
 97+
 98+test("reviews ghostty rather than flagging its output as a filesystem search", async () => {
 99+  expect(
100+    await checkDeterministic(
101+      "bash",
102+      {
103+        command:
104+          "ghostty +show-config --default --docs 2>/dev/null | rg -n -A45 -B4 '^#.*bell-features|bell-features ='",
105+      },
106+      "/",
107+    ),
108+  ).toBeUndefined();
109+});
110+
111 const reviewed = [
112   "rtk ls",
113   'echo "$TARGET"',