chore: promote no-explicit-any from warn to error (#12244)

* chore: promote no-explicit-any from warn to error and resolve violations

Upgrades the oxlint rule severity and removes all 40 existing
`no-explicit-any` warnings across the codebase. Most call sites gained
proper types (SharedEditor refs, JSONNode/JSONMark for ProseMirror JSON
walking, DocumentsStore, dd-trace `Span` parameter inference, prosemirror
Fragment public API in place of internal `(fragment as any).content`).
A few load-bearing `any` uses were preserved with scoped disable
comments where changing the type would cascade widely (Sequelize JSONB
columns on `Event`, the `withTracing` higher-order function generic,
`Extension.options` consumed by many subclasses, dd-trace's `req`
patching).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-05-02 12:14:23 -04:00
committed by GitHub
parent f270611505
commit fca10221b9
24 changed files with 107 additions and 80 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-explicit-any": "warn",
"no-explicit-any": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
+4 -2
View File
@@ -119,10 +119,12 @@ export default class ComponentView {
// Apply classes from inline decorations.
this.decorations.forEach((decoration) => {
// For inline decorations, attrs contain the class property.
const attrs = (decoration as any).type?.attrs;
const attrs = (
decoration as Decoration & { type?: { attrs?: { class?: string } } }
).type?.attrs;
if (attrs?.class) {
const classes = attrs.class.split(" ");
classes.forEach((className: string) => {
classes.forEach((className) => {
if (className && this.dom) {
this.dom.classList.add(className);
}
+3 -3
View File
@@ -345,7 +345,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
}
};
const triggerFilePick = (accept: string, attrs?: Record<string, any>) => {
const triggerFilePick = (accept: string, attrs?: Record<string, unknown>) => {
if (inputRef.current) {
if (accept) {
inputRef.current.accept = accept;
@@ -887,7 +887,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
onPointerMove={handlePointerMove}
onPointerDown={handlePointerDown}
>
{props.renderMenuItem(item as any, index, {
{props.renderMenuItem(item as unknown as T, index, {
selected: index === selectedIndex,
disclosure: hasChildren,
onClick: handleOnClick,
@@ -1053,7 +1053,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
key={`sub-${childIndex}-${child.name}`}
onPointerMove={handleChildPointerMove}
>
{props.renderMenuItem(child as any, childIndex, {
{props.renderMenuItem(child as unknown as T, childIndex, {
selected: childIndex === submenu.selectedIndex,
onClick: handleChildClick,
})}
+5 -6
View File
@@ -1,4 +1,3 @@
import some from "lodash/some";
import { action, observable } from "mobx";
import type { EditorState, Selection } from "prosemirror-state";
import { NodeSelection, Plugin, TextSelection } from "prosemirror-state";
@@ -82,12 +81,12 @@ export default class SelectionToolbarExtension extends Extension {
return false;
}
const slice = selection.content();
const fragment = slice.content;
const nodes = (fragment as any).content;
const fragment = selection.content().content;
if (some(nodes, (n) => n.content.size)) {
return selection;
for (let i = 0; i < fragment.childCount; i++) {
if (fragment.child(i).content.size) {
return selection;
}
}
return false;
+3 -3
View File
@@ -28,9 +28,9 @@ export default class UpArrowAtStart extends Extension {
const isAtDocStart = $pos.parentOffset === 0 && $pos.depth <= 1;
if (isAtDocStart) {
// Call the onUpArrowAtStart callback if it exists
// Cast to any to access the custom prop since it's not in the base Props type
const props = this.editor.props as any;
const props = this.editor.props as {
onUpArrowAtStart?: () => void;
};
if (props.onUpArrowAtStart) {
props.onUpArrowAtStart();
return true;
+12 -7
View File
@@ -38,7 +38,11 @@ import { basicExtensions as extensions } from "@shared/editor/nodes";
import type Node from "@shared/editor/nodes/Node";
import type ReactNode from "@shared/editor/nodes/ReactNode";
import type { ComponentProps } from "@shared/editor/types";
import type { ProsemirrorData, UserPreferences } from "@shared/types";
import type {
ProsemirrorData,
ProsemirrorMark,
UserPreferences,
} from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import EventEmitter from "@shared/utils/events";
import type Document from "~/models/Document";
@@ -117,7 +121,8 @@ export type Props = {
/** Callback when user uses cancel key combo */
onCancel?: () => void;
/** Callback when user changes editor content */
onChange?: (value: () => any) => void;
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
onChange?: (value: (asString?: boolean, trim?: boolean) => any) => void;
/** Callback when a comment mark is clicked */
onClickCommentMark?: (commentId: string) => void;
/**
@@ -755,9 +760,9 @@ export class Editor extends React.PureComponent<
}
if (isArray(node.attrs?.marks)) {
const existingMarks = node.attrs.marks;
const existingMarks = node.attrs.marks as ProsemirrorMark[];
const updatedMarks = existingMarks.filter(
(mark: any) => mark.attrs.id !== commentId
(mark) => mark.attrs?.id !== commentId
);
const attrs = {
...node.attrs,
@@ -800,9 +805,9 @@ export class Editor extends React.PureComponent<
}
if (isArray(node.attrs?.marks)) {
const existingMarks = node.attrs.marks;
const updatedMarks = existingMarks.map((mark: any) =>
mark.type === "comment" && mark.attrs.id === commentId
const existingMarks = node.attrs.marks as ProsemirrorMark[];
const updatedMarks = existingMarks.map((mark) =>
mark.type === "comment" && mark.attrs?.id === commentId
? { ...mark, attrs: { ...mark.attrs, ...attrs } }
: mark
);
+8 -1
View File
@@ -14,6 +14,7 @@ import styled from "styled-components";
import { s } from "@shared/styles";
import { StatusFilter } from "@shared/types";
import type Collection from "~/models/Collection";
import type DocumentsStore from "~/stores/DocumentsStore";
import CenteredContent from "~/components/CenteredContent";
import { CollectionBreadcrumb } from "~/components/CollectionBreadcrumb";
import Heading from "~/components/Heading";
@@ -362,7 +363,13 @@ const Content = styled.div`
`;
const RecentDocuments = observer(
({ collection, documents }: { collection: Collection; documents: any }) => {
({
collection,
documents,
}: {
collection: Collection;
documents: DocumentsStore;
}) => {
useEffect(() => {
void collection.fetchDocuments();
}, [collection]);
+10 -8
View File
@@ -15,6 +15,7 @@ import type { RefHandle } from "~/components/ContentEditable";
import { useDocumentContext } from "~/components/DocumentContext";
import type { Props as EditorProps } from "~/components/Editor";
import Editor from "~/components/Editor";
import type { Editor as SharedEditor } from "~/editor";
import Flex from "~/components/Flex";
import Time from "~/components/Time";
import { withUIExtensions } from "~/editor/extensions";
@@ -59,7 +60,8 @@ type Props = Omit<EditorProps, "editorStyle"> & {
* The main document editor includes an editable title with metadata below it,
* and support for commenting.
*/
function DocumentEditor(props: Props, ref: React.RefObject<any>) {
function DocumentEditor(props: Props, ref: React.ForwardedRef<SharedEditor>) {
const editorRef = React.useRef<SharedEditor>(null);
const titleRef = React.useRef<RefHandle>(null);
const { t } = useTranslation();
const match = useRouteMatch();
@@ -87,10 +89,10 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const iconColor = document.color ?? (first(colorPalette) as string);
const childRef = React.useRef<HTMLDivElement>(null);
const focusAtStart = React.useCallback(() => {
if (ref.current) {
ref.current.focusAtStart();
if (editorRef.current) {
editorRef.current.focusAtStart();
}
}, [ref]);
}, []);
React.useEffect(() => {
if (focusedComment && focusedComment.documentId === document.id) {
@@ -113,15 +115,15 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const handleGoToNextInput = React.useCallback(
(insertParagraph: boolean) => {
if (insertParagraph && ref.current) {
const { view } = ref.current;
if (insertParagraph && editorRef.current) {
const { view } = editorRef.current;
const { dispatch, state } = view;
dispatch(state.tr.insert(0, state.schema.nodes.paragraph.create()));
}
focusAtStart();
},
[focusAtStart, ref]
[focusAtStart]
);
// Create a Comment model in local store when a comment mark is created, this
@@ -231,7 +233,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
/>
) : null}
<EditorComponent
ref={mergeRefs([ref, handleRefChanged])}
ref={mergeRefs([ref, editorRef, handleRefChanged])}
lang={getLangFor(document.language)}
autoFocus={!!document.title && !props.defaultValue}
placeholder={t("Type '/' to insert, or start writing…")}
@@ -13,8 +13,8 @@ type Item = Revision | Event<Document>;
type Props = {
items: Item[];
document: Document;
fetch: (options: Record<string, any> | undefined) => Promise<Item[]>;
options?: Record<string, any>;
fetch: (options: Record<string, unknown> | undefined) => Promise<Item[]>;
options?: Record<string, unknown>;
heading?: React.ReactNode;
empty?: JSX.Element;
};
@@ -7,6 +7,7 @@ import {
useEffect,
forwardRef,
useRef,
type ForwardedRef,
} from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
@@ -18,6 +19,7 @@ import EDITOR_VERSION from "@shared/editor/version";
import { supportsPassiveListener } from "@shared/utils/browser";
import type { Props as EditorProps } from "~/components/Editor";
import Editor from "~/components/Editor";
import type { Editor as SharedEditor } from "~/editor";
import MultiplayerExtension from "~/editor/extensions/Multiplayer";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -50,7 +52,10 @@ type MessageEvent = {
};
};
function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
function MultiplayerEditor(
{ onSynced, ...props }: Props,
ref: ForwardedRef<SharedEditor>
) {
const documentId = props.id;
const history = useHistory();
const { t } = useTranslation();
@@ -352,4 +357,4 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
);
}
export default forwardRef<typeof MultiplayerEditor, Props>(MultiplayerEditor);
export default forwardRef<SharedEditor, Props>(MultiplayerEditor);
+10 -24
View File
@@ -1,6 +1,7 @@
import data from "@emoji-mart/data";
import type { EmojiMartData } from "@emoji-mart/data";
import { Schema } from "prosemirror-model";
import type { Editor } from "~/editor";
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
import { populateEmojiData } from "@shared/editor/lib/emoji";
import {
@@ -12,6 +13,12 @@ import Mention from "@shared/editor/nodes/Mention";
populateEmojiData(data as EmojiMartData);
// Server-side parsing/serializing only requires schema and a few static props,
// but the Extension API expects a full Editor. This stub satisfies bindEditor
// without instantiating the React component.
const stubEditor = (s: Schema): Editor =>
({ schema: s, props: { theme: { isDark: false } } }) as unknown as Editor;
const extensions = withComments(richExtensions);
export const extensionManager = new ExtensionManager(extensions);
@@ -21,14 +28,7 @@ export const schema = new Schema({
});
for (const extension of extensionManager.extensions) {
extension.bindEditor({
schema,
props: {
theme: {
isDark: false,
},
},
} as any);
extension.bindEditor(stubEditor(schema));
}
export const parser = extensionManager.parser({
@@ -48,14 +48,7 @@ export const basicSchema = new Schema({
});
for (const extension of basicExtensionManager.extensions) {
extension.bindEditor({
schema: basicSchema,
props: {
theme: {
isDark: false,
},
},
} as any);
extension.bindEditor(stubEditor(basicSchema));
}
export const basicParser = basicExtensionManager.parser({
@@ -72,14 +65,7 @@ export const commentSchema = new Schema({
});
for (const extension of commentExtensionManager.extensions) {
extension.bindEditor({
schema: commentSchema,
props: {
theme: {
isDark: false,
},
},
} as any);
extension.bindEditor(stubEditor(commentSchema));
}
export const commentParser = commentExtensionManager.parser({
+1 -2
View File
@@ -194,12 +194,11 @@ class Logger {
// Errors have non-enumerable message/stack which are dropped by spreads
// and JSON serialization, so convert them to a plain object up-front.
if (input instanceof Error) {
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
return {
name: input.name,
message: input.message,
stack: input.stack,
} as any as T;
} as unknown as T;
}
// Short circuit if we're not in production to enable easier debugging
+5 -1
View File
@@ -14,6 +14,7 @@ function isExplicitlyNonReportable(error: Error): error is ReportableError {
}
type PrivateDatadogContext = {
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
req: Record<string, any> & {
_datadog?: {
span?: Span;
@@ -41,7 +42,10 @@ const getCurrentSpan = (): Span | null => tracer.scope().active();
* @param tags An object with the tags to add to the span
* @param span An optional span object to add the tags to. If none provided,the current span will be used.
*/
export function addTags(tags: Record<string, any>, span?: Span | null): void {
export function addTags(
tags: Parameters<Span["addTags"]>[0],
span?: Span | null
): void {
if (tracer) {
const currentSpan = span || getCurrentSpan();
+2
View File
@@ -60,6 +60,7 @@ class Event extends IdModel<
* Note that the `data` column will be visible to the client and API requests.
*/
@Column(DataType.JSONB)
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
data: Record<string, any> | null;
/**
@@ -67,6 +68,7 @@ class Event extends IdModel<
* used for arbitrary data associated with the event.
*/
@Column(DataType.JSONB)
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
changes: Record<string, any> | null;
// hooks
+4 -4
View File
@@ -757,7 +757,7 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper {
// Create a new document with the emoji removed from the text
const json = doc.toJSON();
function removeEmojiFromNode(node: any): any {
function removeEmojiFromNode(node: ProsemirrorData): ProsemirrorData {
if (node.type === "text" && node.text && node.text.startsWith(emoji)) {
return {
...node,
@@ -768,7 +768,7 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper {
let found = false;
return {
...node,
content: node.content.map((child: any) => {
content: node.content.map((child) => {
if (found) {
return child;
}
@@ -783,7 +783,7 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper {
return node;
}
const modifiedJson = removeEmojiFromNode(json);
const modifiedJson = removeEmojiFromNode(json as ProsemirrorData);
return {
emoji,
doc: Node.fromJSON(schema, modifiedJson),
@@ -798,7 +798,7 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper {
* @returns A cleanup function to restore the global environment.
*/
public static patchGlobalEnv(domWindow: JSDOM["window"]) {
const g = global as any;
const g = global as unknown as Record<string, unknown>;
const globalParams = {
window: g.window,
+1 -1
View File
@@ -3,7 +3,7 @@ import { BaseTask } from "./base/BaseTask";
type Props = {
templateName: string;
props: Record<string, any>;
props: Record<string, unknown>;
};
export default class EmailTask extends BaseTask<Props> {
+1 -1
View File
@@ -56,7 +56,7 @@ export default function init(
if (ioHandleUpgrade) {
server.removeListener(
"upgrade",
ioHandleUpgrade as (...args: any[]) => void
ioHandleUpgrade as (...args: unknown[]) => void
);
}
+7 -5
View File
@@ -237,7 +237,8 @@ export function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
return instance;
}
const sequelizeVersion = (Sequelize as any).version;
const sequelizeVersion = (Sequelize as unknown as { version: string })
.version;
const major = sequelizeVersion.split(".").map(Number)[0];
if (major >= 7) {
@@ -250,12 +251,13 @@ export function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
}
const origQueryFunc = instance.query.bind(instance);
instance.query = (async (...args: any[]) => {
instance.query = (async (...args: Parameters<typeof origQueryFunc>) => {
try {
return await origQueryFunc(...(args as Parameters<typeof origQueryFunc>));
} catch (err: any) {
return await origQueryFunc(...args);
} catch (err) {
// Ensure error appears in Jest output, not swallowed by Sequelize internals
Logger.error(err.message, err.parent);
const error = err as Error & { parent?: Error };
Logger.error(error.message, error.parent ?? error);
throw err;
}
}) as typeof instance.query;
+2
View File
@@ -86,6 +86,7 @@ export function error(err: unknown): CallToolResult {
* @param handler - the handler function to wrap.
* @returns the wrapped handler with tracing enabled.
*/
/* oxlint-disable @typescript-eslint/no-explicit-any */
export function withTracing<F extends (...args: any[]) => any>(
toolName: string,
handler: F
@@ -107,6 +108,7 @@ export function withTracing<F extends (...args: any[]) => any>(
return handler.apply(this, args);
} as F);
}
/* oxlint-enable @typescript-eslint/no-explicit-any */
/**
* Builds a map from document ID to its zero-based index among siblings,
+5 -2
View File
@@ -23,11 +23,14 @@ export async function getVersionInfo(currentVersion: string): Promise<{
// Continue fetching pages until the required versions are found or no more pages
while (nextUrl) {
const response = await fetch(nextUrl);
const data = await response.json();
const data = (await response.json()) as {
results: { name: string }[];
next?: string | null;
};
// Map and filter the versions to keep only full releases
const pageVersions = data.results
.map((result: any) => result.name)
.map((result) => result.name)
.filter(isFullReleaseVersion);
allVersions = allVersions.concat(pageVersions);
+5 -1
View File
@@ -55,7 +55,11 @@ export function assertKeysIn(
Object.keys(obj).forEach((key) => assertIn(key, Object.values(type)));
}
export const assertSort = (value: string, model: any, message?: string) => {
export const assertSort = (
value: string,
model: { rawAttributes: Record<string, unknown> },
message?: string
) => {
if (!Object.keys(model.rawAttributes).includes(value)) {
throw ValidationError(
message ?? `${String(value)} is not a valid sort field`
+2
View File
@@ -13,9 +13,11 @@ export type WidgetProps = {
};
export default class Extension {
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
options: any;
editor: Editor;
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
constructor(options: Record<string, any> = {}) {
this.options = {
...this.defaultOptions,
+1
View File
@@ -1,3 +1,4 @@
const env = typeof window === "undefined" ? process.env : window.env;
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
export default env as Record<string, any>;
+6 -4
View File
@@ -688,15 +688,17 @@ export type JSONValue =
export type JSONObject = { [x: string]: JSONValue };
export type ProsemirrorMark = {
type: string;
attrs?: JSONObject;
};
export type ProsemirrorData = {
type: string;
content?: ProsemirrorData[];
text?: string;
attrs?: JSONObject;
marks?: {
type: string;
attrs?: JSONObject;
}[];
marks?: ProsemirrorMark[];
};
export type ProsemirrorDoc = {