Kaynağa Gözat

feat: enable oxlint suspicious category, fix 24 violations (#22727)

Kit Langton 18 saat önce
ebeveyn
işleme
702f741267

+ 21 - 1
.oxlintrc.json

@@ -1,5 +1,8 @@
 {
   "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
+  "categories": {
+    "suspicious": "warn"
+  },
   "rules": {
     // Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
     "require-yield": "off",
@@ -10,7 +13,24 @@
     // Intentional control char matching (ANSI escapes, null byte sanitization)
     "no-control-regex": "off",
     // SST and plugin tools require triple-slash references
-    "triple-slash-reference": "off"
+    "triple-slash-reference": "off",
+
+    // Suspicious category: suppress noisy rules
+    // Effect's nested function* closures inherently shadow outer scope
+    "no-shadow": "off",
+    // Namespace-heavy codebase makes this too noisy
+    "unicorn/consistent-function-scoping": "off",
+    // Opinionated — .sort()/.reverse() mutation is fine in this codebase
+    "unicorn/no-array-sort": "off",
+    "unicorn/no-array-reverse": "off",
+    // Not relevant — this isn't a DOM event handler codebase
+    "unicorn/prefer-add-event-listener": "off",
+    // Bundler handles module resolution
+    "unicorn/require-module-specifiers": "off",
+    // postMessage target origin not relevant for this codebase
+    "unicorn/require-post-message-target-origin": "off",
+    // Side-effectful constructors are intentional in some places
+    "no-new": "off"
   },
   "ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"]
 }

+ 2 - 2
github/index.ts

@@ -542,7 +542,7 @@ async function subscribeSessionEvents() {
                     ? JSON.stringify(part.state.input)
                     : "Unknown"
                 console.log()
-                console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
+                console.log(`${color}|`, `\x1b[0m\x1b[2m ${tool.padEnd(7, " ")}`, "", `\x1b[0m${title}`)
               }
 
               if (part.type === "text") {
@@ -776,7 +776,7 @@ async function assertPermissions() {
     console.log(`  permission: ${permission}`)
   } catch (error) {
     console.error(`Failed to check permissions: ${error}`)
-    throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
+    throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error })
   }
 
   if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)

+ 1 - 0
packages/app/src/context/global-sdk.tsx

@@ -128,6 +128,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
       if (started) return run
       started = true
       run = (async () => {
+        // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
         while (!abort.signal.aborted && started) {
           attempt = new AbortController()
           lastEventAt = Date.now()

+ 2 - 0
packages/app/src/utils/runtime-adapters.test.ts

@@ -46,7 +46,9 @@ describe("runtime adapters", () => {
   })
 
   test("resolves speech recognition constructor with webkit precedence", () => {
+    // oxlint-disable-next-line no-extraneous-class
     class SpeechCtor {}
+    // oxlint-disable-next-line no-extraneous-class
     class WebkitCtor {}
     const ctor = getSpeechRecognitionCtor({
       SpeechRecognition: SpeechCtor,

+ 1 - 1
packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx

@@ -90,7 +90,7 @@ export function ReloadSection() {
     }
     const info = billingInfo()!
     setStore("show", true)
-    setStore("reload", info.reload ? true : true)
+    setStore("reload", true)
     setStore("reloadAmount", info.reloadAmount.toString())
     setStore("reloadTrigger", info.reloadTrigger.toString())
   }

+ 1 - 1
packages/desktop-electron/src/main/apps.ts

@@ -28,7 +28,7 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string
     const output = execFileSync("wsl", ["-e", "wslpath", flag, path])
     return output.toString().trim()
   } catch (error) {
-    throw new Error(`Failed to run wslpath: ${String(error)}`)
+    throw new Error(`Failed to run wslpath: ${String(error)}`, { cause: error })
   }
 }
 

+ 1 - 0
packages/function/src/api.ts

@@ -13,6 +13,7 @@ type Env = {
 }
 
 export class SyncServer extends DurableObject<Env> {
+  // oxlint-disable-next-line no-useless-constructor
   constructor(ctx: DurableObjectState, env: Env) {
     super(ctx, env)
   }

+ 1 - 1
packages/opencode/script/postinstall.mjs

@@ -64,7 +64,7 @@ function findBinary() {
 
     return { binaryPath, binaryName }
   } catch (error) {
-    throw new Error(`Could not find package ${packageName}: ${error.message}`)
+    throw new Error(`Could not find package ${packageName}: ${error.message}`, { cause: error })
   }
 }
 

+ 1 - 1
packages/opencode/src/bus/bus-event.ts

@@ -25,7 +25,7 @@ export namespace BusEvent {
             properties: def.properties,
           })
           .meta({
-            ref: "Event" + "." + def.type,
+            ref: `Event.${def.type}`,
           })
       })
       .toArray()

+ 1 - 0
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -111,6 +111,7 @@ function parseToolParams(input?: string) {
       } catch (evalError) {
         throw new Error(
           `Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
+          { cause: evalError },
         )
       }
     }

+ 2 - 1
packages/opencode/src/cli/cmd/github.ts

@@ -1031,6 +1031,7 @@ export const GithubRunCommand = cmd({
           console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error)
           throw new Error(
             "Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.",
+            { cause: error },
           )
         }
       }
@@ -1221,7 +1222,7 @@ export const GithubRunCommand = cmd({
           console.log(`  permission: ${permission}`)
         } catch (error) {
           console.error(`Failed to check permissions: ${error}`)
-          throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
+          throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error })
         }
 
         if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)

+ 1 - 1
packages/opencode/src/cli/cmd/web.ts

@@ -34,7 +34,7 @@ export const WebCommand = cmd({
   describe: "start opencode server and open web interface",
   handler: async (args) => {
     if (!Flag.OPENCODE_SERVER_PASSWORD) {
-      UI.println(UI.Style.TEXT_WARNING_BOLD + "!  " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
+      UI.println(UI.Style.TEXT_WARNING_BOLD + "!  OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
     }
     const opts = await resolveNetworkOptions(args)
     const server = await Server.listen(opts)

+ 1 - 1
packages/opencode/src/patch/patch.ts

@@ -313,7 +313,7 @@ export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFile
   try {
     originalContent = readFileSync(filePath, "utf-8")
   } catch (error) {
-    throw new Error(`Failed to read file ${filePath}: ${error}`)
+    throw new Error(`Failed to read file ${filePath}: ${error}`, { cause: error })
   }
 
   let originalLines = originalContent.split("\n")

+ 1 - 1
packages/opencode/src/server/instance/tui.ts

@@ -339,7 +339,7 @@ export const TuiRoutes = lazy(() =>
                 properties: def.properties,
               })
               .meta({
-                ref: "Event" + "." + def.type,
+                ref: `Event.${def.type}`,
               })
           }),
         ),

+ 1 - 2
packages/opencode/src/session/prompt.ts

@@ -260,8 +260,7 @@ export namespace SessionPrompt {
             messageID: userMessage.info.id,
             sessionID: userMessage.info.sessionID,
             type: "text",
-            text:
-              BUILD_SWITCH + "\n\n" + `A plan file exists at ${plan}. You should execute on the plan defined within it`,
+            text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`,
             synthetic: true,
           })
           userMessage.parts.push(part)

+ 1 - 1
packages/opencode/src/sync/sync-event.ts

@@ -273,7 +273,7 @@ export function payloads() {
           data: def.schema,
         })
         .meta({
-          ref: "SyncEvent" + "." + def.type,
+          ref: `SyncEvent.${def.type}`,
         })
     })
     .toArray()

+ 1 - 1
packages/opencode/src/tool/read.ts

@@ -181,7 +181,7 @@ export const ReadTool = Tool.define(
         )
       }
 
-      let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>" + "\n"].join("\n")
+      let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>\n"].join("\n")
       output += file.raw.map((line, i) => `${i + file.offset}: ${line}`).join("\n")
 
       const last = file.offset + file.raw.length - 1

+ 1 - 1
packages/opencode/src/tool/tool.ts

@@ -78,7 +78,7 @@ export namespace Tool {
   ) {
     return () =>
       Effect.gen(function* () {
-        const toolInfo = init instanceof Function ? { ...(yield* init()) } : { ...init }
+        const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }
         const execute = toolInfo.execute
         toolInfo.execute = (args, ctx) => {
           const attrs = {

+ 3 - 0
packages/opencode/test/mcp/lifecycle.test.ts

@@ -53,6 +53,7 @@ function getOrCreateClientState(name?: string): MockClientState {
 class MockStdioTransport {
   stderr: null = null
   pid = 12345
+  // oxlint-disable-next-line no-useless-constructor
   constructor(_opts: any) {}
   async start() {
     if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
@@ -64,6 +65,7 @@ class MockStdioTransport {
 }
 
 class MockStreamableHTTP {
+  // oxlint-disable-next-line no-useless-constructor
   constructor(_url: URL, _opts?: any) {}
   async start() {
     if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
@@ -76,6 +78,7 @@ class MockStreamableHTTP {
 }
 
 class MockSSE {
+  // oxlint-disable-next-line no-useless-constructor
   constructor(_url: URL, _opts?: any) {}
   async start() {
     if (connectShouldHang) return new Promise<void>(() => {}) // never resolves

+ 5 - 6
packages/opencode/test/session/prompt.test.ts

@@ -60,12 +60,11 @@ function chat(text: string) {
 function hanging(ready: () => void) {
   const encoder = new TextEncoder()
   let timer: ReturnType<typeof setTimeout> | undefined
-  const first =
-    `data: ${JSON.stringify({
-      id: "chatcmpl-1",
-      object: "chat.completion.chunk",
-      choices: [{ delta: { role: "assistant" } }],
-    })}` + "\n\n"
+  const first = `data: ${JSON.stringify({
+    id: "chatcmpl-1",
+    object: "chat.completion.chunk",
+    choices: [{ delta: { role: "assistant" } }],
+  })}\n\n`
   const rest =
     [
       `data: ${JSON.stringify({