build: enable additional oxlint rules

This commit is contained in:
Peter Steinberger
2026-04-23 04:42:54 +01:00
parent dc5ab602df
commit 2e40ca2c15
16 changed files with 103 additions and 73 deletions

View File

@@ -11,10 +11,17 @@
"eslint-plugin-unicorn/prefer-array-find": "error",
"eslint/no-array-constructor": "error",
"eslint/no-await-in-loop": "off",
"eslint/no-div-regex": "error",
"eslint/no-extra-label": "error",
"eslint/no-empty-pattern": ["error", { "allowObjectPatternsAsParameters": false }],
"eslint/no-lone-blocks": "error",
"eslint/no-multi-str": "error",
"eslint/no-new": "error",
"eslint/no-object-constructor": "error",
"eslint/no-proto": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-return-assign": "error",
"eslint/no-sequences": "error",
"eslint/no-shadow": "off",
"eslint/no-useless-call": "error",
"eslint/no-useless-computed-key": "error",
@@ -22,6 +29,9 @@
"eslint/no-useless-constructor": "error",
"eslint/no-warning-comments": "error",
"eslint/no-unmodified-loop-condition": "error",
"eslint/prefer-exponentiation-operator": "error",
"eslint/prefer-numeric-literals": "error",
"eslint/unicode-bom": "error",
"eslint-plugin-unicorn/prefer-set-size": "error",
"oxc/no-accumulating-spread": "error",
"oxc/no-async-endpoint-handlers": "error",
@@ -31,6 +41,7 @@
"typescript/no-explicit-any": "error",
"typescript/no-extraneous-class": "error",
"typescript/no-meaningless-void-operator": "error",
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-arguments": "error",
"typescript/no-unnecessary-type-constraint": "error",
@@ -38,15 +49,20 @@
"typescript/no-unnecessary-type-parameters": "error",
"typescript/no-unsafe-type-assertion": "off",
"typescript/no-useless-default-assignment": "error",
"typescript/prefer-return-this-type": "error",
"typescript/prefer-ts-expect-error": "error",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-unnecessary-array-flat-depth": "error",
"unicorn/no-unnecessary-array-splice-count": "error",
"unicorn/no-unnecessary-slice-end": "error",
"unicorn/no-useless-error-capture-stack-trace": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-set-size": "error",
"unicorn/require-post-message-target-origin": "error"
"unicorn/require-array-join-separator": "error",
"unicorn/require-post-message-target-origin": "error",
"unicorn/throw-new-error": "error"
},
"ignorePatterns": [
"assets/",

View File

@@ -492,7 +492,6 @@ export async function handleFeishuMessage(params: {
}
return;
}
} else {
}
try {

View File

@@ -329,7 +329,7 @@ export async function createMattermostDirectChannelWithRetry(
// Calculate exponential backoff delay with full-jitter
// Jitter is proportional to the exponential delay, not a fixed 1000ms
// This ensures backoff behaves correctly for small delay configurations
const exponentialDelay = initialDelayMs * Math.pow(2, attempt);
const exponentialDelay = initialDelayMs * 2 ** attempt;
const jitter = Math.random() * exponentialDelay;
const delayMs = Math.min(exponentialDelay + jitter, maxDelayMs);

View File

@@ -459,7 +459,7 @@ describe("Reconnect Backoff", () => {
const JITTER = 0.3;
for (let attempt = 0; attempt < 10; attempt++) {
const exponential = BASE * Math.pow(2, attempt);
const exponential = BASE * 2 ** attempt;
const capped = Math.min(exponential, MAX);
const minDelay = capped * (1 - JITTER);
const maxDelay = capped * (1 + JITTER);

View File

@@ -84,9 +84,7 @@ export async function withRetry<T>(
// Schedule the next retry with the configured backoff.
if (attempt < policy.maxRetries) {
const delay =
policy.backoff === "exponential"
? policy.baseDelayMs * Math.pow(2, attempt)
: policy.baseDelayMs;
policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** attempt : policy.baseDelayMs;
logger?.debug?.(
`[qqbot:retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 100)}`,

View File

@@ -119,7 +119,7 @@ export async function sendMessage(
}
if (attempt < maxRetries - 1) {
await sleep(baseDelay * Math.pow(2, attempt));
await sleep(baseDelay * 2 ** attempt);
}
}

View File

@@ -104,7 +104,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
if (attempt >= maxAttempts) {
throw error;
}
const delay = Math.min(30000, 1000 * Math.pow(2, attempt - 1));
const delay = Math.min(30000, 1000 * 2 ** (attempt - 1));
runtime.log?.(`[tlon] Retrying authentication in ${delay}ms...`);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, delay);

View File

@@ -376,7 +376,7 @@ export class UrbitSSEClient {
this.reconnectAttempts += 1;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
this.reconnectDelay * 2 ** (this.reconnectAttempts - 1),
this.maxReconnectDelay,
);

View File

@@ -189,7 +189,7 @@ function scheduleAnnounceDrain(key: string) {
} catch (err) {
queue.consecutiveFailures++;
// Exponential backoff on consecutive failures: 2s, 4s, 8s, ... capped at 60s.
const errorBackoffMs = Math.min(1000 * Math.pow(2, queue.consecutiveFailures), 60_000);
const errorBackoffMs = Math.min(1000 * 2 ** queue.consecutiveFailures, 60_000);
const retryDelayMs = Math.max(errorBackoffMs, queue.debounceMs);
queue.lastEnqueuedAt = Date.now() + retryDelayMs - queue.debounceMs;
defaultRuntime.error?.(

View File

@@ -21,12 +21,12 @@ vi.mock("../../plugin-sdk/facade-runtime.js", async () => {
);
return {
...actual,
tryLoadActivatedBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) => (
(fallbackState.loadCalls += 1),
dirName === fallbackState.activeDirName && fallbackState.resolveSessionConversation
tryLoadActivatedBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) => {
fallbackState.loadCalls += 1;
return dirName === fallbackState.activeDirName && fallbackState.resolveSessionConversation
? { resolveSessionConversation: fallbackState.resolveSessionConversation }
: null
),
: null;
},
};
});

View File

@@ -31,10 +31,10 @@ describe("cloud install docs", () => {
for (const password of KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS) {
expect(markdown, docName).not.toContain(`OPENCLAW_GATEWAY_PASSWORD=${password}`);
}
expect(markdown, docName).not.toMatch(/^ GOG_KEYRING_PASSWORD=change-me-now$/m);
expect(markdown, docName).not.toMatch(/^ {4}GOG_KEYRING_PASSWORD=change-me-now$/m);
if (CLOUD_DOCKER_VM_INSTALL_DOCS.has(docName)) {
expect(markdown, docName).toMatch(/^ OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?$/m);
expect(markdown, docName).toMatch(/^ GOG_KEYRING_PASSWORD=[ \t]*\r?$/m);
expect(markdown, docName).toMatch(/^ {4}OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?$/m);
expect(markdown, docName).toMatch(/^ {4}GOG_KEYRING_PASSWORD=[ \t]*\r?$/m);
expect(markdown, docName).toContain("openssl rand -hex 32");
}
}

View File

@@ -184,33 +184,27 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
await res.text();
}
{
await expectAgentSessionKeyMatch({
body: { model: "openclaw", messages: [{ role: "user", content: "hi" }] },
headers: { "x-openclaw-agent-id": "beta" },
matcher: /^agent:beta:/,
});
}
await expectAgentSessionKeyMatch({
body: { model: "openclaw", messages: [{ role: "user", content: "hi" }] },
headers: { "x-openclaw-agent-id": "beta" },
matcher: /^agent:beta:/,
});
{
await expectAgentSessionKeyMatch({
body: {
model: "openclaw/beta",
messages: [{ role: "user", content: "hi" }],
},
matcher: /^agent:beta:/,
});
}
await expectAgentSessionKeyMatch({
body: {
model: "openclaw/beta",
messages: [{ role: "user", content: "hi" }],
},
matcher: /^agent:beta:/,
});
{
await expectAgentSessionKeyMatch({
body: {
model: "openclaw/default",
messages: [{ role: "user", content: "hi" }],
},
matcher: /^agent:main:/,
});
}
await expectAgentSessionKeyMatch({
body: {
model: "openclaw/default",
messages: [{ role: "user", content: "hi" }],
},
matcher: /^agent:main:/,
});
{
mockAgentOnce([{ text: "hello" }]);
@@ -417,19 +411,17 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
await res.text();
}
{
await expectInvalidRequestNoDispatch([
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: "https://example.com/image.png" },
},
],
},
]);
}
await expectInvalidRequestNoDispatch([
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: "https://example.com/image.png" },
},
],
},
]);
{
mockAgentOnce([{ text: "I can see the image" }]);
@@ -522,19 +514,17 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
await res.text();
}
{
await expectInvalidRequestNoDispatch([
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: "data:application/pdf;base64,QUJDRA==" },
},
],
},
]);
}
await expectInvalidRequestNoDispatch([
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: "data:application/pdf;base64,QUJDRA==" },
},
],
},
]);
{
const manyImageParts = Array.from({ length: 9 }).map(() => ({

View File

@@ -522,7 +522,7 @@ describe("diagnostic support export", () => {
>;
expect(Object.getPrototypeOf(snapshot)).toBe(null);
expect(snapshot.__proto__).toBeUndefined();
expect(Object.hasOwn(snapshot, "__proto__")).toBe(false);
expect(snapshot.constructor).toBeUndefined();
expect(snapshot.prototype).toBeUndefined();
expect(snapshot.field0000).toBe(0);

View File

@@ -358,7 +358,7 @@ function sanitizeCommandArguments(args: unknown[], redaction: SupportRedactionCo
if (!hasInlineValue) {
redactNext = true;
}
return hasInlineValue ? arg.replace(/=.*/u, "=<redacted>") : arg;
return hasInlineValue ? arg.replace(/[=].*/u, "=<redacted>") : arg;
}
return redactSupportString(arg, redaction);
});

View File

@@ -11,6 +11,25 @@ type OxlintTsconfig = {
exclude?: string[];
};
const ZERO_BASELINE_RULES = [
"eslint/no-div-regex",
"eslint/no-extra-label",
"eslint/no-lone-blocks",
"eslint/no-multi-str",
"eslint/no-proto",
"eslint/no-regex-spaces",
"eslint/no-sequences",
"eslint/prefer-exponentiation-operator",
"eslint/prefer-numeric-literals",
"eslint/unicode-bom",
"typescript/no-non-null-asserted-nullish-coalescing",
"typescript/prefer-return-this-type",
"unicorn/no-useless-error-capture-stack-trace",
"unicorn/prefer-optional-catch-binding",
"unicorn/require-array-join-separator",
"unicorn/throw-new-error",
];
function readJson(path: string): unknown {
return JSON.parse(fs.readFileSync(path, "utf8")) as unknown;
}
@@ -68,4 +87,12 @@ describe("oxlint config", () => {
{ allowInterfaces: "with-single-extends" },
]);
});
it("enables clean zero-baseline lint rules", () => {
const config = readJson(".oxlintrc.json") as OxlintConfig;
for (const rule of ZERO_BASELINE_RULES) {
expect(config.rules?.[rule]).toBe("error");
}
});
});

View File

@@ -139,7 +139,7 @@ describe("prototype pollution prevention", () => {
const obj: Record<string, unknown> = {};
setPathValue(obj, ["__proto__", "polluted"], true);
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
expect(obj.__proto__).toBe(Object.prototype);
expect(Object.getPrototypeOf(obj)).toBe(Object.prototype);
});
it("setPathValue rejects constructor in path", () => {