chore: resolve lint warnings (no-explicit-any, no-redundant-type-constituents, no-base-to-string) (#12209)

* chore: resolve no-redundant-type-constituents and test/mock no-explicit-any warnings

Clears 36 lint warnings: all 5 no-redundant-type-constituents, 6
no-misused-spread (via narrowing getPartitionWhereClause's return type
to WhereAttributeHash), and 25 no-explicit-any in test/mock files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: resolve no-base-to-string warnings in tests

Convert userProvisioner try/catch error assertions to Jest's
.rejects.toThrow() idiom, and cast webhook test body to string.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: resolve no-explicit-any warnings in cancan and tracing

Tighten types in the cancan policy framework and tracing decorators.
Constructor / generic-function upper bounds keep `any` where TypeScript
variance requires it, scoped to single-line oxlint-disable comments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-04-28 22:55:30 -04:00
committed by GitHub
parent f8e70c2c39
commit 57308c46af
10 changed files with 72 additions and 87 deletions
@@ -59,8 +59,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
const { presence, auth, ui } = useStores();
const [editorVersionBehind, setEditorVersionBehind] = useState(false);
const [showCursorNames, setShowCursorNames] = useState(false);
const [remoteProvider, setRemoteProvider] =
useState<HocuspocusProvider | null>(null);
const [remoteProvider, setRemoteProvider] = useState<HocuspocusProvider>();
const [hasLocalPersistence, setHasLocalPersistence] = useState(true);
const [isLocalSynced, setLocalSynced] = useState(false);
const [isRemoteSynced, setRemoteSynced] = useState(false);
@@ -223,7 +222,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
window.removeEventListener("scroll", syncScrollPosition);
provider?.destroy();
void localProvider?.destroy();
setRemoteProvider(null);
setRemoteProvider(undefined);
ui.setMultiplayerStatus(undefined, undefined);
};
}, [
@@ -44,9 +44,7 @@ describe("DeliverWebhookTask", () => {
"http://example.com",
expect.anything()
);
const parsedBody = JSON.parse(
fetchMock.mock.calls[0]![1]!.body!.toString()
);
const parsedBody = JSON.parse(fetchMock.mock.calls[0]![1]!.body as string);
expect(parsedBody.webhookSubscriptionId).toBe(subscription.id);
expect(parsedBody.event).toBe("users.signin");
expect(parsedBody.payload.id).toBe(signedInUser.id);
@@ -120,9 +118,7 @@ describe("DeliverWebhookTask", () => {
"http://example.com",
expect.anything()
);
const parsedBody = JSON.parse(
fetchMock.mock.calls[0]![1]!.body!.toString()
);
const parsedBody = JSON.parse(fetchMock.mock.calls[0]![1]!.body as string);
expect(parsedBody.webhookSubscriptionId).toBe(subscription.id);
expect(parsedBody.event).toBe("users.delete");
expect(parsedBody.payload.id).toBe(deletedUserId);
+3 -3
View File
@@ -15,7 +15,7 @@ export default class Queue {
return 0;
}
add = function (data: any) {
add = function (data: unknown) {
const job = this.createJob(data);
if (!this.handler) {
@@ -25,7 +25,7 @@ export default class Queue {
this.handler(job, this.done);
};
process = function (handler: any) {
process = function (handler: (job: unknown, done: () => void) => void) {
if (this.handler) {
throw Error("Cannot define a handler more than once per Queue instance");
}
@@ -33,7 +33,7 @@ export default class Queue {
this.handler = handler;
};
createJob = function (data: any) {
createJob = function (data: unknown) {
return {
data,
};
+6 -6
View File
@@ -5,18 +5,18 @@ import type { Tracer } from "dd-trace";
const emptyFn = function () {};
const callableHandlers = {
get<T, P extends keyof T>(_target: T, _prop: P, _receiver: any): T[P] {
get<T, P extends keyof T>(_target: T, _prop: P, _receiver: unknown): T[P] {
const newMock = new Proxy(emptyFn, callableHandlers);
return newMock as any as T[P];
return newMock as unknown as T[P];
},
apply<T extends (...args: any) => any, A extends Parameters<T>>(
apply<T extends (...args: never[]) => unknown, A extends Parameters<T>>(
_target: T,
_thisArg: any,
_thisArg: unknown,
_args: A
): ReturnType<T> {
const newMock = new Proxy(emptyFn, callableHandlers);
return newMock as any as ReturnType<T>;
return newMock as unknown as ReturnType<T>;
},
};
@@ -31,7 +31,7 @@ export const mockTracer = new Proxy({} as MockTracer, {
}
if (key === "wrap") {
return (_: any, f: any) => f;
return (_: unknown, f: unknown) => f;
}
return callableMock;
+12 -31
View File
@@ -308,10 +308,9 @@ describe("userProvisioner", () => {
const authenticationProviders = await team.$get("authenticationProviders");
const authenticationProvider = authenticationProviders[0];
let error;
try {
await userProvisioner(ctx, {
await expect(
userProvisioner(ctx, {
name: "Uninvited User",
email: "invite@ExamPle.com",
teamId: team.id,
@@ -321,14 +320,8 @@ describe("userProvisioner", () => {
accessToken: "123",
scopes: ["read"],
},
});
} catch (err) {
error = err;
}
expect(error && error.toString()).toContain(
"You need an invite to join this team"
);
})
).rejects.toThrow("You need an invite to join this team");
});
it("should create a user from allowed domain", async () => {
@@ -389,19 +382,14 @@ describe("userProvisioner", () => {
it("should not create a user with emailMatchOnly when no allowed domains are set", async () => {
const team = await buildTeam();
let error;
try {
await userProvisioner(ctx, {
await expect(
userProvisioner(ctx, {
name: "Test Name",
email: faker.internet.email(),
teamId: team.id,
});
} catch (err) {
error = err;
}
expect(error && error.toString()).toContain("UnauthorizedError");
})
).rejects.toThrow("No matching user for email or allowed domain");
});
it("should reject an user when the domain is not allowed", async () => {
@@ -415,10 +403,9 @@ describe("userProvisioner", () => {
const authenticationProviders = await team.$get("authenticationProviders");
const authenticationProvider = authenticationProviders[0];
let error;
try {
await userProvisioner(ctx, {
await expect(
userProvisioner(ctx, {
name: "Bad Domain User",
email: faker.internet.email(),
teamId: team.id,
@@ -428,13 +415,7 @@ describe("userProvisioner", () => {
accessToken: "123",
scopes: ["read"],
},
});
} catch (err) {
error = err;
}
expect(error && error.toString()).toContain(
"The domain is not allowed for this workspace"
);
})
).rejects.toThrow("The domain is not allowed for this workspace");
});
});
+17 -11
View File
@@ -29,13 +29,14 @@ import * as Tracing from "./tracer";
type DDTag = (typeof DDTags)[keyof typeof DDTags];
type Tags = {
[tag in DDTag]?: any;
[tag in DDTag]?: unknown;
} & {
[key: string]: any;
[key: string]: unknown;
};
interface Constructor {
new (...args: any[]): any;
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- variance requires `any[]` to accept arbitrary constructors
new (...args: any[]): unknown;
}
interface TraceConfig {
@@ -58,6 +59,7 @@ interface TraceConfig {
export const traceFunction =
(config: TraceConfig) =>
<
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- variance requires `any` to accept arbitrary functions
F extends (...args: any[]) => any,
P extends Parameters<F>,
R extends ReturnType<F>,
@@ -66,7 +68,7 @@ export const traceFunction =
): F =>
env.ENVIRONMENT === "test"
? target
: (function wrapperFn(this: any, ...args: P): R {
: (function wrapperFn(this: unknown, ...args: P): R {
const { className, methodName = target.name, tags } = config;
const childOf = config.isRoot
? undefined
@@ -125,8 +127,8 @@ export const traceFunction =
} as F);
const traceMethod = (config?: TraceConfig) =>
function <R, A extends any[], F extends (...args: A) => R>(
target: any,
function <R, A extends unknown[], F extends (...args: A) => R>(
target: { name?: string; constructor: { name: string } },
_propertyKey: string,
descriptor: PropertyDescriptor
): TypedPropertyDescriptor<F> {
@@ -191,20 +193,24 @@ const traceClass = (config?: TraceConfig) =>
export function trace(config?: TraceConfig) {
function traceDecorator(target: Constructor): void;
function traceDecorator<T>(
target: Record<string, any>,
target: Record<string, unknown>,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
): void;
function traceDecorator(
a: Constructor | Record<string, any>,
b?: any,
c?: any
a: Constructor | Record<string, unknown>,
b?: string | symbol,
c?: PropertyDescriptor
): void {
if (typeof a === "function") {
// Need to cast as there is no safe runtime way to check if a function is a constructor
traceClass(config)(a as Constructor);
} else {
traceMethod(config)(a, b, c);
traceMethod(config)(
a as { name?: string; constructor: { name: string } },
b as string,
c!
);
}
}
+6 -5
View File
@@ -2,14 +2,15 @@ import isObject from "lodash/isPlainObject";
import type { Model } from "sequelize-typescript";
import { AuthorizationError } from "@server/errors";
type Constructor = new (...args: any) => any;
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- variance requires `any` to accept arbitrary constructors
type Constructor = new (...args: any[]) => unknown;
type Policy = Record<string, boolean | string[]>;
type Condition<T extends Constructor, P extends Constructor> = (
performer: InstanceType<P>,
target: InstanceType<T> | null,
options?: any
options?: unknown
) => boolean | string;
type Ability = {
@@ -132,7 +133,7 @@ export class CanCan {
if (performer instanceof model) {
for (const [action, abilities] of actionMap.entries()) {
for (const ability of abilities) {
if (target instanceof (ability.target as any)) {
if (target instanceof (ability.target as Constructor)) {
actionsToCheck.add(action);
break;
}
@@ -211,7 +212,7 @@ export class CanCan {
if (
ability.target === "all" ||
target === ability.target ||
target instanceof (ability.target as any)
target instanceof (ability.target as Constructor)
) {
matchingAbilities.push(ability);
}
@@ -225,7 +226,7 @@ export class CanCan {
if (
ability.target === "all" ||
target === ability.target ||
target instanceof (ability.target as any)
target instanceof (ability.target as Constructor)
) {
matchingAbilities.push(ability);
}
+18 -16
View File
@@ -3,6 +3,8 @@ import { Minute } from "@shared/utils/time";
import type { PartitionInfo } from "./CronTask";
import { CronTask, TaskInterval } from "./CronTask";
type RangeWhere = Record<string, { [Op.gte]: string; [Op.lte]: string }>;
// Create a concrete implementation of CronTask for testing
class TestTask extends CronTask {
public async perform() {
@@ -18,8 +20,8 @@ class TestTask extends CronTask {
public testPartitionWhereClause(
idField: string,
partition: PartitionInfo | undefined
) {
return this.getPartitionWhereClause(idField, partition);
): RangeWhere {
return this.getPartitionWhereClause(idField, partition) as RangeWhere;
}
}
@@ -105,7 +107,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: 0,
partitionCount: 3,
}) as any;
});
expect(where).toBeDefined();
expect(where.id).toBeDefined();
@@ -117,17 +119,17 @@ describe("CronTask", () => {
const where0 = task.testPartitionWhereClause("id", {
partitionIndex: 0,
partitionCount: 3,
}) as any;
});
const where1 = task.testPartitionWhereClause("id", {
partitionIndex: 1,
partitionCount: 3,
}) as any;
});
const where2 = task.testPartitionWhereClause("id", {
partitionIndex: 2,
partitionCount: 3,
}) as any;
});
// Partition 0: Should start from 00000000
expect(where0.id[Op.gte]).toBe("00000000-0000-4000-8000-000000000000");
@@ -146,12 +148,12 @@ describe("CronTask", () => {
const where0 = task.testPartitionWhereClause("id", {
partitionIndex: 0,
partitionCount: 2,
}) as any;
});
const where1 = task.testPartitionWhereClause("id", {
partitionIndex: 1,
partitionCount: 2,
}) as any;
});
// Partition 0: 0x00000000 to 0x7fffffff
expect(where0.id[Op.gte]).toBe("00000000-0000-4000-8000-000000000000");
@@ -170,7 +172,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: i,
partitionCount,
}) as any;
});
ranges.push({
start: where.id[Op.gte],
end: where.id[Op.lte],
@@ -199,7 +201,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: 0,
partitionCount: 1,
}) as any;
});
// Should cover entire UUID space
expect(where.id[Op.gte]).toBe("00000000-0000-4000-8000-000000000000");
@@ -233,12 +235,12 @@ describe("CronTask", () => {
const where1 = task.testPartitionWhereClause("id", {
partitionIndex: 0,
partitionCount: 2,
}) as any;
});
const where2 = task.testPartitionWhereClause("documentId", {
partitionIndex: 0,
partitionCount: 2,
}) as any;
});
expect(where1.id).toBeDefined();
expect(where1.documentId).toBeUndefined();
@@ -254,7 +256,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: i,
partitionCount,
}) as any;
});
ranges.push({
start: where.id[Op.gte],
end: where.id[Op.lte],
@@ -276,7 +278,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: 1,
partitionCount: 16, // 16 partitions = 0x10000000 per partition
}) as any;
});
// Partition 1 should be from 0x10000000 to 0x1fffffff
expect(where.id[Op.gte]).toBe("10000000-0000-4000-8000-000000000000");
@@ -304,7 +306,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: i,
partitionCount,
}) as any;
});
const startUuid = where.id[Op.gte];
const endUuid = where.id[Op.lte];
@@ -334,7 +336,7 @@ describe("CronTask", () => {
const where = task.testPartitionWhereClause("id", {
partitionIndex: i,
partitionCount,
}) as any;
});
ranges.push({
start: where.id[Op.gte],
end: where.id[Op.lte],
+2 -2
View File
@@ -1,4 +1,4 @@
import type { WhereOptions } from "sequelize";
import type { WhereAttributeHash } from "sequelize";
import { Op } from "sequelize";
import { Minute } from "@shared/utils/time";
import { BaseTask } from "./BaseTask";
@@ -178,7 +178,7 @@ export abstract class CronTask extends BaseTask<Props> {
protected getPartitionWhereClause(
idField: string,
partitionInfo: PartitionInfo | undefined
): WhereOptions {
): WhereAttributeHash {
if (!partitionInfo) {
return {};
}
+4 -4
View File
@@ -3,19 +3,19 @@ import type MarkdownIt from "markdown-it";
const CHECKBOX_REGEX = /\[(X|\s|_|-)\]\s(.*)?/i;
function matches(token: Token | undefined) {
function matches(token: Token) {
return token && token.content.match(CHECKBOX_REGEX);
}
function isInline(token: Token | undefined): boolean {
function isInline(token: Token): boolean {
return !!token && token.type === "inline";
}
function isParagraph(token: Token | undefined): boolean {
function isParagraph(token: Token): boolean {
return !!token && token.type === "paragraph_open";
}
function isListItem(token: Token | undefined): boolean {
function isListItem(token: Token): boolean {
// Only match list_item_open, not checkbox_item_open - items that are already
// checkbox_item_open have been processed (e.g., by the tables rule for
// checkboxes in table cells) and should not be processed again.